Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0527649
WIP: tanstack automigartion
huang-julien Apr 28, 2026
3b9c9f7
Merge remote-tracking branch 'origin/next' into feat/tanstack_automig…
huang-julien May 5, 2026
26fa32f
refactor: write prompt into cp
huang-julien May 5, 2026
3e9e4b7
chore: updaet rompt
huang-julien May 5, 2026
acb41b5
docs: update migration md
huang-julien May 5, 2026
1881de6
Merge branch 'next' into feat/tanstack_automigrate
huang-julien May 5, 2026
bcbd705
fix: lint + test
huang-julien May 5, 2026
b24e31b
Merge branch 'feat/tanstack_automigrate' of https://github.com/storyb…
huang-julien May 5, 2026
f792937
test: fix and add catch
huang-julien May 5, 2026
456b390
apply coderabbit suggestions
huang-julien May 5, 2026
ab9f5bc
Update code/lib/cli-storybook/src/automigrate/fixes/react-vite-to-tan…
huang-julien May 5, 2026
30d4774
apply suggestion
huang-julien May 5, 2026
2729a90
Merge branch 'feat/tanstack_automigrate' of https://github.com/storyb…
huang-julien May 5, 2026
308a0de
chore: format
huang-julien May 5, 2026
8cc0214
Merge branch 'next' into feat/tanstack_automigrate
huang-julien May 6, 2026
09ff270
test: update tests
huang-julien May 6, 2026
f6a49a5
Merge branch 'feat/tanstack_automigrate' of https://github.com/storyb…
huang-julien May 6, 2026
161cbd2
docs: update tanstack-react documentation for automigration
huang-julien May 6, 2026
a8323ee
chore: format
huang-julien May 6, 2026
10e242b
feat: always log the prompt
huang-julien May 6, 2026
79c0db7
Docs tweaks
kylegach May 6, 2026
a9ca55d
chore: move tinyclip to devdeps
huang-julien May 7, 2026
4f9b1d6
docs: add link
huang-julien May 7, 2026
5456279
Merge branch 'next' into feat/tanstack_automigrate
huang-julien May 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -552,6 +553,65 @@ export default {
};
```

### TanStack Router projects: migrate from `@storybook/react-vite` to `@storybook/tanstack-react`
Comment thread
huang-julien marked this conversation as resolved.

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<typeof Route>;

export default meta;
export const Default: StoryObj<typeof meta> = {};
```

`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
Expand Down
1 change: 1 addition & 0 deletions code/lib/cli-storybook/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
2 changes: 2 additions & 0 deletions code/lib/cli-storybook/src/automigrate/fixes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -38,6 +39,7 @@ export const allFixes: Fix[] = [
rnOndeviceAddonsToDeviceAddons,
migrateAddonConsole,
nextjsToNextjsVite,
reactViteToTanstackReact,
removeAddonInteractions,
rendererToFramework,
removeEssentials,
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <RouterProvider router={router}><Story /></RouterProvider>;
},
];
`);

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 <RouterProvider router={router}><Story /></RouterProvider>;
};
`);

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();
});
});
});
Loading
Loading