diff --git a/MIGRATION.md b/MIGRATION.md index 6e2d8f7a8a5c..6dffa77bd1f3 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -2,6 +2,7 @@ - [From version 10.3.0 to 10.4.0](#from-version-1030-to-1040) - [React Native: on-device addons moved to `deviceAddons`](#react-native-on-device-addons-moved-to-deviceaddons) + - [TanStack React: migrate from `@storybook/react-vite` to `@storybook/tanstack-react`](#tanstack-router-projects-migrate-from-storybookreact-vite-to-storybooktanstack-react) - [From version 10.0.0 to 10.1.0](#from-version-1000-to-1010) - [API and Component Changes](#api-and-component-changes) - [Button Component API Changes](#button-component-api-changes) @@ -552,6 +553,65 @@ export default { }; ``` +### TanStack Router projects: migrate from `@storybook/react-vite` to `@storybook/tanstack-react` + +Projects using `@storybook/react-vite` together with `@tanstack/react-router` (or `@tanstack/react-start`) can migrate to the dedicated `@storybook/tanstack-react` framework. The new framework provides built-in TanStack Router and TanStack Query support: every story is automatically wrapped in a TanStack Router instance (in-memory history), so any manual decorator that creates a `RouterProvider`, `createRouter`, `createMemoryHistory` or `createRootRoute` is no longer needed and should be removed. + +To run the automigration: + +```sh +npx storybook automigrate +``` + +The migration will: + +- Replace `@storybook/react-vite` with `@storybook/tanstack-react` in `package.json`. +- Update the framework string in `.storybook/main.*` (works for both regular and CSF factories `defineMain` configs). +- Update import sites that reference `@storybook/react-vite` (including CSF factories `definePreview` and `defineMain` from `@storybook/react-vite/node`). +- Detect manual TanStack Router decorators in `.storybook/preview.*`, the rest of `.storybook/`, and any `*.stories.*` file. When a decorator is detected, the migration offers to copy a ready-to-paste AI prompt to your clipboard that walks an AI assistant through removing it. + +After running the automigration, remove any manual TanStack Router decorators (you can use the AI prompt offered by the CLI). For stories that need to render under a specific route, use the framework's `parameters.tanstack.router` API instead. With the CSF factories syntax: + +```ts +// src/stories/Page.stories.ts +import preview from '#.storybook/preview'; +import { Route } from './Page'; + +const meta = preview.meta({ + title: 'Example/Page', + parameters: { + tanstack: { + router: { + route: Route, + }, + }, + }, +}); + +export const Default = meta.story(); +``` + +Or with the CSF3 syntax — note that `Meta` and `StoryObj` should now be imported from `@storybook/tanstack-react` so they pick up the TanStack-aware parameter types: + +```ts +import type { Meta, StoryObj } from '@storybook/tanstack-react'; +import { Route } from './Page'; + +const meta = { + title: 'Example/Page', + parameters: { + tanstack: { router: { route: Route } }, + }, +} satisfies Meta; + +export default meta; +export const Default: StoryObj = {}; +``` + +`parameters.tanstack.router` also accepts `path`, `params`, `query`, `routeOverrides`, and `useRouterContext` for finer-grained control. You can additionally register a project-wide default route by passing `route` to `definePreview` in `.storybook/preview.*`. + +See the [`@storybook/tanstack-react` framework documentation](https://storybook.js.org/docs/get-started/frameworks/tanstack-react) for the full list of features and APIs. + ## From version 10.0.0 to 10.1.0 ### API and Component Changes diff --git a/code/lib/cli-storybook/package.json b/code/lib/cli-storybook/package.json index ad2c4b563063..25cbfcb6aa10 100644 --- a/code/lib/cli-storybook/package.json +++ b/code/lib/cli-storybook/package.json @@ -59,6 +59,7 @@ "semver": "^7.7.3", "slash": "^5.0.0", "tiny-invariant": "^1.3.3", + "tinyclip": "^0.1.12", "typescript": "^5.8.3" }, "publishConfig": { diff --git a/code/lib/cli-storybook/src/automigrate/fixes/index.ts b/code/lib/cli-storybook/src/automigrate/fixes/index.ts index 98caa846d01d..bfe4e23874a9 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/index.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/index.ts @@ -12,6 +12,7 @@ import { fixFauxEsmRequire } from './fix-faux-esm-require.ts'; import { initialGlobals } from './initial-globals.ts'; import { migrateAddonConsole } from './migrate-addon-console.ts'; import { nextjsToNextjsVite } from './nextjs-to-nextjs-vite.ts'; +import { reactViteToTanstackReact } from './react-vite-to-tanstack-react.ts'; import { removeAddonInteractions } from './remove-addon-interactions.ts'; import { removeDocsAutodocs } from './remove-docs-autodocs.ts'; import { removeEssentials } from './remove-essentials.ts'; @@ -38,6 +39,7 @@ export const allFixes: Fix[] = [ rnOndeviceAddonsToDeviceAddons, migrateAddonConsole, nextjsToNextjsVite, + reactViteToTanstackReact, removeAddonInteractions, rendererToFramework, removeEssentials, diff --git a/code/lib/cli-storybook/src/automigrate/fixes/react-vite-to-tanstack-react.test.ts b/code/lib/cli-storybook/src/automigrate/fixes/react-vite-to-tanstack-react.test.ts new file mode 100644 index 000000000000..74d23715fac9 --- /dev/null +++ b/code/lib/cli-storybook/src/automigrate/fixes/react-vite-to-tanstack-react.test.ts @@ -0,0 +1,262 @@ +import { readFile, writeFile } from 'node:fs/promises'; + +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +// eslint-disable-next-line depend/ban-dependencies +import { globby } from 'globby'; +import type { JsPackageManager } from 'storybook/internal/common'; +import { transformImportFiles } from 'storybook/internal/common'; +import { logger, prompt } from 'storybook/internal/node-logger'; +import { writeText } from 'tinyclip'; + +import type { CheckOptions } from './index.ts'; +import { + REACT_VITE_PACKAGE, + TANSTACK_REACT_PACKAGE, + reactViteToTanstackReact, +} from './react-vite-to-tanstack-react.ts'; + +vi.mock('node:fs/promises', { spy: true }); +vi.mock('storybook/internal/node-logger', { spy: true }); +vi.mock('storybook/internal/common', { spy: true }); +vi.mock('globby', { spy: true }); +vi.mock('tinyclip', { spy: true }); + +const mockReadFile = vi.mocked(readFile); +const mockWriteFile = vi.mocked(writeFile); + +describe('react-vite-to-tanstack-react', () => { + const mockPackageManager = { + getAllDependencies: vi.fn(), + packageJsonPaths: ['/project/package.json'], + removeDependencies: vi.fn().mockResolvedValue(undefined), + addDependencies: vi.fn().mockResolvedValue(undefined), + getDependencyVersion: vi.fn(), + } as unknown as JsPackageManager; + + beforeEach(() => { + vi.clearAllMocks(); + // Reset default behaviours for spied modules so spies don't call through to real impls. + mockReadFile.mockResolvedValue(''); + mockWriteFile.mockResolvedValue(undefined); + vi.mocked(globby).mockResolvedValue([]); + vi.mocked(writeText).mockResolvedValue(undefined); + vi.mocked(transformImportFiles).mockResolvedValue([]); + vi.mocked(logger.step).mockImplementation(() => {}); + vi.mocked(logger.debug).mockImplementation(() => {}); + vi.mocked(logger.warn).mockImplementation(() => {}); + vi.mocked(logger.log).mockImplementation(() => {}); + vi.mocked(logger.error).mockImplementation(() => {}); + vi.mocked(logger.logBox).mockImplementation(() => {}); + vi.mocked(prompt.confirm).mockResolvedValue(false); + vi.mocked(mockPackageManager.removeDependencies).mockResolvedValue(undefined); + vi.mocked(mockPackageManager.addDependencies).mockResolvedValue(undefined); + }); + + describe('check function', () => { + it('returns null if @storybook/react-vite is not installed', async () => { + vi.mocked(mockPackageManager.getAllDependencies).mockReturnValue({ + '@tanstack/react-router': '^1.0.0', + }); + + const result = await reactViteToTanstackReact.check({ + packageManager: mockPackageManager, + } as CheckOptions); + + expect(result).toBeNull(); + }); + + it('returns null if no tanstack router package is installed', async () => { + vi.mocked(mockPackageManager.getAllDependencies).mockReturnValue({ + [REACT_VITE_PACKAGE]: '^9.0.0', + }); + + const result = await reactViteToTanstackReact.check({ + packageManager: mockPackageManager, + } as CheckOptions); + + expect(result).toBeNull(); + }); + + it('returns options when both react-vite and a tanstack router package are present', async () => { + vi.mocked(mockPackageManager.getAllDependencies).mockReturnValue({ + [REACT_VITE_PACKAGE]: '^10.0.0', + '@tanstack/react-router': '^1.0.0', + }); + + const result = await reactViteToTanstackReact.check({ + packageManager: mockPackageManager, + previewConfigPath: undefined, + } as CheckOptions); + + expect(result).toEqual({ + hasTanstackRouterDecorator: false, + }); + }); + + it('detects a manual tanstack router decorator in the preview config', async () => { + vi.mocked(mockPackageManager.getAllDependencies).mockReturnValue({ + [REACT_VITE_PACKAGE]: '^10.0.0', + '@tanstack/react-router': '^1.0.0', + }); + + // preview file + mockReadFile.mockResolvedValueOnce(` + import { RouterProvider, createMemoryHistory, createRouter } from '@tanstack/react-router'; + + export const decorators = [ + (Story) => { + const router = createRouter({ history: createMemoryHistory() }); + return ; + }, + ]; + `); + + const result = await reactViteToTanstackReact.check({ + packageManager: mockPackageManager, + previewConfigPath: '/project/.storybook/preview.tsx', + } as CheckOptions); + + expect(result?.hasTanstackRouterDecorator).toBe(true); + }); + + it('detects a tanstack router decorator that lives in a separate config file', async () => { + vi.mocked(mockPackageManager.getAllDependencies).mockReturnValue({ + [REACT_VITE_PACKAGE]: '^10.0.0', + '@tanstack/react-router': '^1.0.0', + }); + + vi.mocked(globby).mockResolvedValueOnce([ + '/project/.storybook/preview.tsx', + '/project/.storybook/decorators.tsx', + ]); + + // preview.tsx — only imports the decorator, no router markers itself + mockReadFile.mockResolvedValueOnce(` + import { withRouter } from './decorators'; + export const decorators = [withRouter]; + `); + // decorators.tsx — the actual router setup lives here + mockReadFile.mockResolvedValueOnce(` + import { RouterProvider, createRouter, createMemoryHistory } from '@tanstack/react-router'; + + export const withRouter = (Story) => { + const router = createRouter({ history: createMemoryHistory() }); + return ; + }; + `); + + const result = await reactViteToTanstackReact.check({ + packageManager: mockPackageManager, + previewConfigPath: '/project/.storybook/preview.tsx', + configDir: '/project/.storybook', + storiesPaths: [], + } as unknown as CheckOptions); + + expect(result?.hasTanstackRouterDecorator).toBe(true); + }); + }); + + describe('prompt function', () => { + it('mentions both packages', () => { + const message = reactViteToTanstackReact.prompt(); + expect(message).toContain(REACT_VITE_PACKAGE); + expect(message).toContain(TANSTACK_REACT_PACKAGE); + }); + }); + + describe('run function', () => { + it('updates dependencies and rewrites the framework string in main config', async () => { + mockReadFile.mockResolvedValueOnce(` + import { defineMain } from '${REACT_VITE_PACKAGE}/node'; + export default defineMain({ framework: '${REACT_VITE_PACKAGE}' }); + `); + + await reactViteToTanstackReact.run!({ + result: { + hasTanstackRouterDecorator: false, + }, + dryRun: false, + packageManager: mockPackageManager, + mainConfigPath: '/project/.storybook/main.ts', + previewConfigPath: '/project/.storybook/preview.tsx', + storiesPaths: [], + configDir: '.storybook', + storybookVersion: '10.1.0', + } as any); + + expect(mockPackageManager.removeDependencies).toHaveBeenCalledWith([REACT_VITE_PACKAGE]); + expect(mockPackageManager.addDependencies).toHaveBeenCalledWith( + { type: 'devDependencies', skipInstall: true }, + [`${TANSTACK_REACT_PACKAGE}@10.1.0`] + ); + expect(mockWriteFile).toHaveBeenCalledWith( + '/project/.storybook/main.ts', + expect.stringContaining(TANSTACK_REACT_PACKAGE) + ); + // Ensure no leftover @storybook/react-vite reference (handles CSF factories /node export too) + const writtenContent = mockWriteFile.mock.calls[0]?.[1] as string; + expect(writtenContent).not.toContain(REACT_VITE_PACKAGE); + expect(writtenContent).toContain(`${TANSTACK_REACT_PACKAGE}/node`); + }); + + it('skips writes in dry run mode', async () => { + await reactViteToTanstackReact.run!({ + result: { + hasTanstackRouterDecorator: false, + }, + dryRun: true, + packageManager: mockPackageManager, + mainConfigPath: '/project/.storybook/main.ts', + previewConfigPath: '/project/.storybook/preview.tsx', + storiesPaths: [], + configDir: '.storybook', + storybookVersion: '10.1.0', + } as any); + + expect(mockPackageManager.removeDependencies).not.toHaveBeenCalled(); + expect(mockPackageManager.addDependencies).not.toHaveBeenCalled(); + }); + + it('asks the user for an AI prompt when a decorator is detected', async () => { + vi.mocked(prompt.confirm).mockResolvedValueOnce(true); + + mockReadFile.mockResolvedValueOnce('export default {};'); + + await reactViteToTanstackReact.run!({ + result: { + hasTanstackRouterDecorator: true, + }, + dryRun: false, + packageManager: mockPackageManager, + mainConfigPath: '/project/.storybook/main.ts', + previewConfigPath: '/project/.storybook/preview.tsx', + storiesPaths: [], + configDir: '.storybook', + storybookVersion: '10.1.0', + } as any); + + expect(prompt.confirm).toHaveBeenCalled(); + }); + + it('does not prompt for AI when --yes is passed', async () => { + mockReadFile.mockResolvedValueOnce('export default {};'); + + await reactViteToTanstackReact.run!({ + result: { + hasTanstackRouterDecorator: true, + }, + dryRun: false, + packageManager: mockPackageManager, + mainConfigPath: '/project/.storybook/main.ts', + previewConfigPath: '/project/.storybook/preview.tsx', + storiesPaths: [], + configDir: '.storybook', + storybookVersion: '10.1.0', + yes: true, + } as any); + + expect(prompt.confirm).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/code/lib/cli-storybook/src/automigrate/fixes/react-vite-to-tanstack-react.ts b/code/lib/cli-storybook/src/automigrate/fixes/react-vite-to-tanstack-react.ts new file mode 100644 index 000000000000..a6dc060e3465 --- /dev/null +++ b/code/lib/cli-storybook/src/automigrate/fixes/react-vite-to-tanstack-react.ts @@ -0,0 +1,422 @@ +import { readFile, writeFile } from 'node:fs/promises'; + +import { transformImportFiles } from 'storybook/internal/common'; +import { logger, prompt } from 'storybook/internal/node-logger'; +import { writeText } from 'tinyclip'; +import picocolors from 'picocolors'; +import { dedent } from 'ts-dedent'; + +import type { Fix } from '../types.ts'; + +export const REACT_VITE_PACKAGE = '@storybook/react-vite'; +export const TANSTACK_REACT_PACKAGE = '@storybook/tanstack-react'; + +interface ReactViteToTanstackReactOptions { + /** Whether the preview config appears to set up a TanStack Router decorator manually. */ + hasTanstackRouterDecorator: boolean; +} + +/** Markers that strongly suggest a manual TanStack Router decorator is configured in preview/stories. */ +const TANSTACK_ROUTER_DECORATOR_MARKERS = [ + 'createMemoryHistory', + 'createRootRoute', + 'createRouter', + 'RouterProvider', +]; + +const TANSTACK_ROUTER_PACKAGES = [ + '@tanstack/react-router', + '@tanstack/router-core', + '@tanstack/start', + '@tanstack/react-start', +]; + +const fileLooksLikeTanstackRouterDecorator = (content: string): boolean => { + const importsTanstackRouter = TANSTACK_ROUTER_PACKAGES.some((pkg) => + new RegExp(`from\\s+['"]${pkg.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\$&')}['"]`).test(content) + ); + if (!importsTanstackRouter) { + return false; + } + return TANSTACK_ROUTER_DECORATOR_MARKERS.some((marker) => content.includes(marker)); +}; + +/** + * Detect a manual TanStack Router decorator anywhere in the user's Storybook surface area: + * + * - The preview file itself + * - Any file inside the Storybook config directory (decorators are often factored out into + * `./decorators.ts` or `./withRouter.tsx` and imported by `preview.ts`) + * - Any *.stories.* file (per-story `decorators: [...]`) + * + * We can't trace arbitrary user imports outside the config dir, but covering these locations + * catches the vast majority of real-world setups. + */ +const detectTanstackRouterDecorator = async ({ + previewConfigPath, + configDir, + storiesPaths, +}: { + previewConfigPath: string | undefined; + configDir: string | undefined; + storiesPaths: string[]; +}): Promise => { + // eslint-disable-next-line depend/ban-dependencies + const { globby } = await import('globby'); + + const configFiles = configDir + ? await globby([`${configDir}/**/*.{ts,tsx,js,jsx,mjs,cjs}`], { + ignore: ['**/node_modules/**', '**/dist/**'], + }) + : []; + + const candidateFiles = Array.from( + new Set([...(previewConfigPath ? [previewConfigPath] : []), ...configFiles, ...storiesPaths]) + ); + + for (const file of candidateFiles) { + try { + const content = await readFile(file, 'utf-8'); + if (fileLooksLikeTanstackRouterDecorator(content)) { + return true; + } + } catch { + continue; + } + } + return false; +}; + +const transformMainConfig = async (mainConfigPath: string, dryRun: boolean): Promise => { + try { + const content = await readFile(mainConfigPath, 'utf-8'); + + if (!content.includes(REACT_VITE_PACKAGE)) { + return false; + } + + const transformedContent = content.replaceAll(REACT_VITE_PACKAGE, TANSTACK_REACT_PACKAGE); + + if (transformedContent !== content && !dryRun) { + await writeFile(mainConfigPath, transformedContent); + } + + return transformedContent !== content; + } catch (error) { + logger.error(`Failed to update main config at ${mainConfigPath}: ${error}`); + return false; + } +}; + +const buildAiMigrationPrompt = (previewConfigPath?: string) => + dedent` + You are migrating a Storybook project from the "${REACT_VITE_PACKAGE}" framework to the + "${TANSTACK_REACT_PACKAGE}" framework. The framework swap in .storybook/main.* and the + package.json dependency change have already been performed by the Storybook automigration + CLI — do not redo those. Your job is to clean up the user-land code that the CLI cannot + safely transform. + + Reference documentation: + https://storybook.js.org/docs/get-started/frameworks/tanstack-react + + # Background + + "${TANSTACK_REACT_PACKAGE}" is a TanStack Router-aware Storybook framework built on top + of @storybook/react-vite. It mounts every story inside a TanStack Router (in-memory + history) automatically. Manual router setup that users previously wired into a + decorator is now redundant and must be removed — leaving it in place creates a nested + router and breaks stories. + + Telltale signs of a manual decorator that must be removed (any of these in + preview / decorator / story files): + - imports from "@tanstack/react-router", "@tanstack/router-core", + "@tanstack/start", or "@tanstack/react-start" used to construct a router + - calls to createMemoryHistory(...), createRootRoute(...), createRouter(...) + - JSX usage of + - a decorator function (e.g. const withRouter = (Story) => ) + wired into "decorators: [...]" of a meta or preview + + # The replacement APIs from "${TANSTACK_REACT_PACKAGE}" + + All of the following are exported from "${TANSTACK_REACT_PACKAGE}" — re-exporting the + @storybook/react primitives plus the TanStack-specific additions. Use these instead of + a hand-rolled router decorator. Only opt in when a story actually needs a specific + route — by default the framework's auto-router is enough. + + 1) Preview-level default route (CSF factories — recommended). + + In ${previewConfigPath ?? '.storybook/preview.*'}: + + import { definePreview } from '@storybook/tanstack-react'; + import { routeTree } from '../src/routeTree.gen'; // or a custom Route + + export default definePreview({ + // optional — registers a default route for every story in the project + route: routeTree, + parameters: { /* ... */ }, + }); + + 2) Per-meta route via "parameters.tanstack.router" (works with both CSF factories + and CSF3). Most TanStack-Router-decorator removals translate to this: + + // CSF factories style (preferred when the project already uses preview.meta): + import preview from '#.storybook/preview'; + import { Route } from './Page'; + + const meta = preview.meta({ + title: 'Example/Page', + parameters: { + tanstack: { + router: { + route: Route, + }, + }, + }, + }); + + export const Default = meta.story(); + + // CSF3 style: + import type { Meta, StoryObj } from '@storybook/tanstack-react'; + import { Route } from './Page'; + + const meta = { + title: 'Example/Page', + parameters: { + tanstack: { router: { route: Route } }, + }, + } satisfies Meta; + + export default meta; + export const Default: StoryObj = {}; + + 3) Per-story override — same shape, but on a single story's "parameters" instead of + the meta. Useful when one story needs a different route than the others. + + 4) Additional "parameters.tanstack.router" fields you can use when needed: + - "path" initial pathname (e.g. '/users/$id') + - "params" URL params object (typed against the route) + - "query" search params object (typed against the route) + - "routeOverrides" map of route id -> { loader / beforeLoad / validateSearch / ... } + for mocking specific routes in the registered tree + - "useRouterContext" function that returns a custom router context per story + + Do NOT introduce any of these unless the original decorator was clearly setting up + that exact behavior. Remove the decorator first; only port over the bits that were + actually doing work. + + 5) Type imports. If the project uses CSF3 types, switch them from "@storybook/react" + (or "@storybook/react-vite") to "@storybook/tanstack-react": + + - import type { Meta, StoryObj } from '@storybook/react'; + + import type { Meta, StoryObj } from '@storybook/tanstack-react'; + + The TanStack-aware "Meta" infers TanStack parameter types from the route in + "component" / parameters, giving type-safe params/query. + + # Tasks (perform in order, in the user's repository) + + 1. Open ${previewConfigPath ?? '.storybook/preview.*'} and remove any TanStack Router + decorator from the "decorators" array. The decorator may be: + - defined inline in this file, or + - imported from another module (commonly .storybook/decorators.ts, + .storybook/withRouter.tsx, or similar) — in that case remove BOTH the + import + usage here AND delete the decorator definition at its source. + If the source decorator file becomes empty after removal, delete the file. + + Confirm "definePreview" is imported from "@storybook/tanstack-react" (not + "@storybook/react" or "@storybook/react-vite"). If the project has a single + project-wide route to register, pass it via "definePreview({ route })". + + 2. Search the whole repository (not just .storybook/) for any *.stories.* file that + declares a per-story or per-meta TanStack Router decorator and remove it. If the + removed decorator was setting up a specific route, port that to + "parameters.tanstack.router" using the APIs above (route / path / params / query / + routeOverrides — whichever applies). + + 3. Drop now-unused imports of: RouterProvider, createRouter, createMemoryHistory, + createRootRoute, Outlet (from "@tanstack/react-router" / "@tanstack/router-core" / + "@tanstack/start" / "@tanstack/react-start"). Keep imports that are still + legitimately used elsewhere in the file (Link, useNavigate, createRoute when used + to build a Route passed to "parameters.tanstack.router.route", etc.). + + 4. Update story-type imports from "@storybook/react" / "@storybook/react-vite" to + "@storybook/tanstack-react" wherever the story uses TanStack router parameters, + so the types pick up the TanStack additions. + + 5. Preserve CSF factories syntax. If the file uses + "definePreview({...})" / "preview.meta({...})" / "meta.story(...)", keep that shape; + only mutate the affected fields. Do not rewrite CSF1/CSF2/CSF3 -> CSF factories or + vice versa. + + 6. Preserve all other decorators, parameters, args, argTypes, loaders, beforeEach, + tags, and globals exactly as they were. Only the TanStack Router decorator is + being removed. + + 7. Do not edit .storybook/main.*, package.json, or any lockfile — those are already + handled by the automigration CLI. + + # Verification checklist before finishing + + - No remaining manual "RouterProvider", "createRouter", "createMemoryHistory", + "createRootRoute" usage in preview, .storybook/**, or *.stories.*. + - "decorators" arrays no longer contain the removed TanStack Router decorator. + - All previously imported router symbols that are no longer referenced are gone. + - Story files importing TanStack-aware types use "@storybook/tanstack-react". + - TypeScript still compiles. Storybook still loads. Stories that don't need a + specific route now rely on the framework's default in-memory router; stories + that do specify a route do so via "parameters.tanstack.router". + `; + +export const reactViteToTanstackReact: Fix = { + id: 'react-vite-to-tanstack-react', + link: 'https://storybook.js.org/docs/get-started/frameworks/tanstack-react', + defaultSelected: false, + + async check({ + packageManager, + previewConfigPath, + configDir, + storiesPaths, + }): Promise { + const allDeps = packageManager.getAllDependencies(); + + const hasReactVitePackage = !!allDeps[REACT_VITE_PACKAGE]; + const hasTanstackRouter = TANSTACK_ROUTER_PACKAGES.some((pkg) => !!allDeps[pkg]); + + if (!hasReactVitePackage || !hasTanstackRouter) { + return null; + } + + const hasTanstackRouterDecorator = await detectTanstackRouterDecorator({ + previewConfigPath, + configDir, + storiesPaths: storiesPaths ?? [], + }); + + return { + hasTanstackRouterDecorator, + }; + }, + + prompt() { + return `Migrate from ${REACT_VITE_PACKAGE} to ${TANSTACK_REACT_PACKAGE} (TanStack Router-aware framework)`; + }, + + async run({ + result, + dryRun = false, + mainConfigPath, + previewConfigPath, + storiesPaths, + configDir, + packageManager, + storybookVersion, + yes, + }) { + if (!result) { + return; + } + + logger.step(`Migrating from ${REACT_VITE_PACKAGE} to ${TANSTACK_REACT_PACKAGE}...`); + + if (dryRun) { + logger.debug('Dry run: Skipping package.json updates.'); + } else { + logger.debug('Updating package.json files...'); + await packageManager.removeDependencies([REACT_VITE_PACKAGE]); + await packageManager.addDependencies({ type: 'devDependencies', skipInstall: true }, [ + `${TANSTACK_REACT_PACKAGE}@${storybookVersion}`, + ]); + } + + if (mainConfigPath) { + logger.debug('Updating main config file...'); + await transformMainConfig(mainConfigPath, dryRun); + } + + logger.debug('Scanning and updating import statements...'); + + // eslint-disable-next-line depend/ban-dependencies + const { globby } = await import('globby'); + const configFiles = configDir + ? await globby([`${configDir}/**/*.{ts,tsx,js,jsx,mjs,cjs}`], { + ignore: ['**/node_modules/**', '**/dist/**'], + }) + : []; + const allFiles = [...storiesPaths, ...configFiles, previewConfigPath].filter( + Boolean + ) as string[]; + + const transformErrors = await transformImportFiles( + allFiles, + { + [REACT_VITE_PACKAGE]: TANSTACK_REACT_PACKAGE, + }, + !!dryRun + ); + + if (transformErrors.length > 0) { + logger.warn(`Encountered ${transformErrors.length} errors during file transformation:`); + transformErrors.forEach(({ file, error }) => { + logger.warn(` - ${file}: ${error.message}`); + }); + } + + if (result.hasTanstackRouterDecorator) { + logger.logBox( + dedent` + We detected what looks like a manual TanStack Router decorator in + ${picocolors.cyan(previewConfigPath ?? '.storybook/preview')}. + + ${picocolors.bold(TANSTACK_REACT_PACKAGE)} wraps every story in a TanStack Router + automatically (see ${picocolors.yellow( + 'https://storybook.js.org/docs/get-started/frameworks/tanstack-react' + )}), so that decorator is no longer needed and should be removed. + ` + ); + + const wantsAiPrompt = yes + ? false + : await prompt.confirm({ + message: + 'Would you like a ready-to-paste AI prompt to help remove the now-unused TanStack Router decorator?', + initialValue: true, + }); + + if (wantsAiPrompt) { + const aiPrompt = buildAiMigrationPrompt(previewConfigPath); + const separator = picocolors.dim('─'.repeat(60)); + + let clipboardOk = false; + try { + await writeText(aiPrompt); + clipboardOk = true; + } catch { + // Clipboard access can fail in CI / headless Linux environments where the + // platform helper (e.g. `xclip`) isn't installed. We fall back to printing + // only — the prompt is logged below either way. + } + + // Always log the prompt so coding agents running this automigration can read it + // directly from stdout (no clipboard available in agentic environments). Humans + // benefit too: the clipboard contents are visible for verification. + logger.logBox( + dedent`${ + clipboardOk + ? 'AI migration prompt copied to clipboard. Full prompt below:' + : 'Clipboard not available in this environment. Copy the AI migration prompt below manually:' + } + + ${separator} + ${aiPrompt} + ${separator}` + ); + } + } + logger.step('Migration completed successfully!'); + logger.log( + `For more information, see: https://storybook.js.org/docs/get-started/frameworks/tanstack-react` + ); + }, +}; diff --git a/docs/get-started/frameworks/tanstack-react.mdx b/docs/get-started/frameworks/tanstack-react.mdx index fefa12cfa107..43e98aca8eca 100644 --- a/docs/get-started/frameworks/tanstack-react.mdx +++ b/docs/get-started/frameworks/tanstack-react.mdx @@ -191,15 +191,36 @@ In individual stories, use [`loaders`](../../writing-stories/loaders.mdx) to cal ## FAQ -### How do I manually install the TanStack React framework? +### How do I migrate from the `react-vite` framework? -To migrate to `@storybook/tanstack-react` from `@storybook/react-vite` or to set it up manually, follow these steps: +#### Automatic migration + +Storybook provides a migration tool for migrating to this framework from the React (Vite) framework, [`@storybook/react-vite`](./react-vite.mdx). To migrate, run this command: + +```bash +npx storybook automigrate react-vite-to-tanstack-react +``` + +This automigration tool performs the following actions: + +1. Updates `package.json` files to replace `@storybook/react-vite` with `@storybook/tanstack-react`. +2. Updates `.storybook/main.js|ts` to change the framework property (works with both regular and CSF factories `defineMain` configs). +3. Scans and updates import statements that reference `@storybook/react-vite` in your story files and Storybook configuration files (including `@storybook/react-vite/node` used by CSF factories). +4. Detects manual TanStack Router decorators in `.storybook/preview.*`, the rest of `.storybook/`, and any `*.stories.*` file. When one is found, the CLI offers to copy a ready-to-paste AI prompt to your clipboard that walks an AI assistant through removing the now-redundant decorator. + + + +`@storybook/tanstack-react` already wraps every story in a TanStack Router automatically, so any manual `RouterProvider` / `createRouter` / `createMemoryHistory` / `createRootRoute` decorator should be removed after running the automigration. For stories that need a specific route, use [`parameters.tanstack.router`](#rendering-a-route) instead. + + + +#### Manual migration First, install the framework: -Then, update your `.storybook/main.js|ts` to set the framework: +Then, update your `.storybook/main.js|ts` to change the framework property: @@ -207,6 +228,12 @@ Then similarly update your `.storybook/preview.*` to import from `@storybook/tan + + +`@storybook/tanstack-react` already wraps every story in a TanStack Router automatically, so any manual `RouterProvider` / `createRouter` / `createMemoryHistory` / `createRootRoute` decorator should be removed after running the automigration. For stories that need a specific route, use [`parameters.tanstack.router`](#rendering-a-route) instead. + + + ### When should I use `@storybook/tanstack-react` instead of `@storybook/react-vite`? Use `@storybook/tanstack-react` when your components rely on TanStack Router or TanStack Start APIs and you want Storybook to provide router context, typed route parameters, automatic router mocking, and mocked TanStack Start server-function behavior. diff --git a/yarn.lock b/yarn.lock index 04b2df155270..66b6a6002c4c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8504,6 +8504,7 @@ __metadata: slash: "npm:^5.0.0" storybook: "workspace:*" tiny-invariant: "npm:^1.3.3" + tinyclip: "npm:^0.1.12" ts-dedent: "npm:^2.0.0" typescript: "npm:^5.8.3" bin: @@ -30542,6 +30543,13 @@ __metadata: languageName: node linkType: hard +"tinyclip@npm:^0.1.12": + version: 0.1.12 + resolution: "tinyclip@npm:0.1.12" + checksum: 10c0/5319ca4d430cc7cafe9624b86ba331467e0f6c718b2a961e3fc2a48bff932a319b1aaf8bd7f8edb70f4bef24e233e442e4fc9ffa77c994bace415324e0bc86cf + languageName: node + linkType: hard + "tinyexec@npm:^0.3.0": version: 0.3.2 resolution: "tinyexec@npm:0.3.2"