Merge pull request #30882 from storybookjs/shilman/rnstorybook-automigration

CLI: Add React Native `.rnstorybook` CLI automigration
This commit is contained in:
Michael Shilman 2025-03-24 23:48:02 +08:00 committed by GitHub
commit 04fc157e04
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 282 additions and 12 deletions

View File

@ -1,6 +1,7 @@
<h1>Migration</h1>
- [From version 8.x to 9.0.0](#from-version-8x-to-900)
- [React-Native config dir renamed](#react-native-config-dir-renamed)
- [Addon viewport and addon backgrounds synchronized configuration and use globals](#addon-viewport-and-addon-backgrounds-synchronized-configuration-and-use-globals)
- [Manager builder removed alias for `util`, `assert` and `process`](#manager-builder-removed-alias-for-util-assert-and-process)
- [Actions addon moved to core](#actions-addon-moved-to-core)
@ -125,17 +126,17 @@
- [Tab addons cannot manually route, Tool addons can filter their visibility via tabId](#tab-addons-cannot-manually-route-tool-addons-can-filter-their-visibility-via-tabid)
- [Removed `config` preset](#removed-config-preset-1)
- [From version 7.5.0 to 7.6.0](#from-version-750-to-760)
- [CommonJS with Vite is deprecated](#commonjs-with-vite-is-deprecated)
- [Using implicit actions during rendering is deprecated](#using-implicit-actions-during-rendering-is-deprecated)
- [typescript.skipBabel deprecated](#typescriptskipbabel-deprecated)
- [Primary doc block accepts of prop](#primary-doc-block-accepts-of-prop)
- [Addons no longer need a peer dependency on React](#addons-no-longer-need-a-peer-dependency-on-react)
- [CommonJS with Vite is deprecated](#commonjs-with-vite-is-deprecated)
- [Using implicit actions during rendering is deprecated](#using-implicit-actions-during-rendering-is-deprecated)
- [typescript.skipBabel deprecated](#typescriptskipbabel-deprecated)
- [Primary doc block accepts of prop](#primary-doc-block-accepts-of-prop)
- [Addons no longer need a peer dependency on React](#addons-no-longer-need-a-peer-dependency-on-react)
- [From version 7.4.0 to 7.5.0](#from-version-740-to-750)
- [`storyStoreV6` and `storiesOf` is deprecated](#storystorev6-and-storiesof-is-deprecated)
- [`storyIndexers` is replaced with `experimental_indexers`](#storyindexers-is-replaced-with-experimental_indexers)
- [`storyStoreV6` and `storiesOf` is deprecated](#storystorev6-and-storiesof-is-deprecated)
- [`storyIndexers` is replaced with `experimental_indexers`](#storyindexers-is-replaced-with-experimental_indexers)
- [From version 7.0.0 to 7.2.0](#from-version-700-to-720)
- [Addon API is more type-strict](#addon-api-is-more-type-strict)
- [Addon-controls hideNoControlsWarning parameter is deprecated](#addon-controls-hidenocontrolswarning-parameter-is-deprecated)
- [Addon API is more type-strict](#addon-api-is-more-type-strict)
- [Addon-controls hideNoControlsWarning parameter is deprecated](#addon-controls-hidenocontrolswarning-parameter-is-deprecated)
- [From version 6.5.x to 7.0.0](#from-version-65x-to-700)
- [7.0 breaking changes](#70-breaking-changes)
- [Dropped support for Node 15 and below](#dropped-support-for-node-15-and-below)
@ -161,7 +162,7 @@
- [Deploying build artifacts](#deploying-build-artifacts)
- [Dropped support for file URLs](#dropped-support-for-file-urls)
- [Serving with nginx](#serving-with-nginx)
- [Ignore story files from node\_modules](#ignore-story-files-from-node_modules)
- [Ignore story files from node_modules](#ignore-story-files-from-node_modules)
- [7.0 Core changes](#70-core-changes)
- [7.0 feature flags removed](#70-feature-flags-removed)
- [Story context is prepared before for supporting fine grained updates](#story-context-is-prepared-before-for-supporting-fine-grained-updates)
@ -175,7 +176,7 @@
- [Addon-interactions: Interactions debugger is now default](#addon-interactions-interactions-debugger-is-now-default)
- [7.0 Vite changes](#70-vite-changes)
- [Vite builder uses Vite config automatically](#vite-builder-uses-vite-config-automatically)
- [Vite cache moved to node\_modules/.cache/.vite-storybook](#vite-cache-moved-to-node_modulescachevite-storybook)
- [Vite cache moved to node_modules/.cache/.vite-storybook](#vite-cache-moved-to-node_modulescachevite-storybook)
- [7.0 Webpack changes](#70-webpack-changes)
- [Webpack4 support discontinued](#webpack4-support-discontinued)
- [Babel mode v7 exclusively](#babel-mode-v7-exclusively)
@ -226,7 +227,7 @@
- [Dropped addon-docs manual babel configuration](#dropped-addon-docs-manual-babel-configuration)
- [Dropped addon-docs manual configuration](#dropped-addon-docs-manual-configuration)
- [Autoplay in docs](#autoplay-in-docs)
- [Removed STORYBOOK\_REACT\_CLASSES global](#removed-storybook_react_classes-global)
- [Removed STORYBOOK_REACT_CLASSES global](#removed-storybook_react_classes-global)
- [7.0 Deprecations and default changes](#70-deprecations-and-default-changes)
- [storyStoreV7 enabled by default](#storystorev7-enabled-by-default)
- [`Story` type deprecated](#story-type-deprecated)
@ -441,6 +442,13 @@
## From version 8.x to 9.0.0
### React-Native config dir renamed
In Storybook 9, React Native (RN) projects use the `.rnstorybook` config directory instead of `.storybook`.
That makes it easier for RN and React Native Web (RNW) storybooks to co-exist in the same project.
To upgrade, either rename your `.storybook` directory to `.rnstorybook` or if you wish to continue using `.storybook` (not recommended), you can use the [`configPath`](https://github.com/storybookjs/react-native#configpath) option to specify `.storybook` manually.
### Addon viewport and addon backgrounds synchronized configuration and use globals
The feature flags: `viewportStoryGlobals` and `backgroundsStoryGlobals` have been removed, please remove these from your `.storybook/main.ts` file.

View File

@ -49,6 +49,7 @@
"create-storybook": "workspace:*",
"cross-spawn": "^7.0.3",
"envinfo": "^7.7.3",
"execa": "^9.5.2",
"fd-package-json": "^1.2.0",
"find-up": "^5.0.0",
"giget": "^1.0.0",

View File

@ -23,6 +23,7 @@ import { removeArgtypesRegex } from './remove-argtypes-regex';
import { removedGlobalClientAPIs } from './remove-global-client-apis';
import { removeLegacyMDX1 } from './remove-legacymdx1';
import { rendererToFramework } from './renderer-to-framework';
import { rnstorybookConfig } from './rnstorybook-config';
import { sbBinary } from './sb-binary';
import { sbScripts } from './sb-scripts';
import { storyshotsMigration } from './storyshots-migration';
@ -70,6 +71,7 @@ export const allFixes: Fix[] = [
consolidatedImports,
addonExperimentalTest,
rendererToFramework,
rnstorybookConfig,
];
export const initFixes: Fix[] = [eslintPlugin];

View File

@ -0,0 +1,158 @@
import { existsSync } from 'node:fs';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { StorybookConfigRaw } from 'storybook/internal/types';
// eslint-disable-next-line depend/ban-dependencies
import { $ } from 'execa';
// eslint-disable-next-line depend/ban-dependencies
import { globby } from 'globby';
import { makePackageManager } from '../helpers/testing-helpers';
import { rnstorybookConfig } from './rnstorybook-config';
const { check } = rnstorybookConfig;
const mockMainConfig: StorybookConfigRaw = {
stories: ['../stories/**/*.stories.@(js|jsx|ts|tsx)'],
};
vi.mock('node:fs');
vi.mock('execa');
vi.mock('globby');
describe('react-native-config fix', () => {
beforeEach(() => {
vi.mocked($).mockClear();
// @ts-expect-error blah
vi.mocked($).mockResolvedValue({ stdout: '' });
vi.mocked(globby).mockResolvedValue(['storybook.requires.ts']);
});
describe('no-ops', () => {
it('when @storybook/react-native is not installed', async () => {
const packageManager = makePackageManager({
devDependencies: {},
});
await expect(
check({
packageManager,
mainConfigPath: '.storybook/main.js',
mainConfig: mockMainConfig,
storybookVersion: '8.0.0',
})
).resolves.toBeNull();
});
it('when .storybook directory does not exist', async () => {
const packageManager = makePackageManager({
devDependencies: {
'@storybook/react-native': '^8.0.0',
},
});
vi.mocked(existsSync).mockReturnValue(false);
await expect(
check({
packageManager,
mainConfigPath: '.storybook/main.js',
mainConfig: mockMainConfig,
storybookVersion: '8.0.0',
})
).resolves.toBeNull();
});
it('when .rnstorybook directory already exists', async () => {
const packageManager = makePackageManager({
devDependencies: {
'@storybook/react-native': '^8.0.0',
},
});
vi.mocked(existsSync).mockReturnValue(true);
await expect(
check({
packageManager,
mainConfigPath: '.storybook/main.js',
mainConfig: mockMainConfig,
storybookVersion: '8.0.0',
})
).resolves.toBeNull();
});
it('when @storybook/react-native is installed and .storybook exists but no requires file', async () => {
const packageManager = makePackageManager({
devDependencies: {
'@storybook/react-native': '^8.0.0',
},
});
// Mock existsSync to return true for .storybook and false for .rnstorybook
vi.mocked(existsSync).mockImplementation((path) => path.toString().includes('.storybook'));
vi.mocked(globby).mockResolvedValue([]);
const result = await check({
packageManager,
mainConfigPath: '.storybook/main.js',
mainConfig: mockMainConfig,
storybookVersion: '8.0.0',
});
expect(result).toBeNull();
});
});
describe('continue', () => {
it('when @storybook/react-native is installed and .storybook exists', async () => {
const packageManager = makePackageManager({
devDependencies: {
'@storybook/react-native': '^8.0.0',
},
});
// Mock existsSync to return true for .storybook and false for .rnstorybook
vi.mocked(existsSync).mockImplementation((path) => path.toString().includes('.storybook'));
const result = await check({
packageManager,
mainConfigPath: '.storybook/main.js',
mainConfig: mockMainConfig,
storybookVersion: '8.0.0',
});
expect(result).toEqual({
storybookDir: expect.stringContaining('.storybook'),
rnStorybookDir: expect.stringContaining('.rnstorybook'),
dotStorybookReferences: [],
});
});
it('when there are references to .storybook in the project', async () => {
// @ts-expect-error blah
vi.mocked($).mockResolvedValue({ stdout: 'a\nb\nc' });
const packageManager = makePackageManager({
devDependencies: {
'@storybook/react-native': '^8.0.0',
},
});
// Mock existsSync to return true for .storybook and false for .rnstorybook
vi.mocked(existsSync).mockImplementation((path) => path.toString().includes('.storybook'));
const result = await check({
packageManager,
mainConfigPath: '.storybook/main.js',
mainConfig: mockMainConfig,
storybookVersion: '8.0.0',
});
expect(result).toEqual({
storybookDir: expect.stringContaining('.storybook'),
rnStorybookDir: expect.stringContaining('.rnstorybook'),
dotStorybookReferences: ['a', 'b', 'c'],
});
});
});
});

View File

@ -0,0 +1,100 @@
import { existsSync } from 'node:fs';
import { readFile, rename, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import picocolors from 'picocolors';
import { dedent } from 'ts-dedent';
import type { Fix } from '../types';
interface Options {
dotStorybookReferences: string[];
storybookDir: string;
rnStorybookDir: string;
}
/** Replaces all occurrences of a string in a file with another string */
async function renameInFile(filePath: string, oldText: string, newText: string): Promise<void> {
try {
const content = await readFile(filePath, 'utf8');
const updatedContent = content.replaceAll(oldText, newText);
await writeFile(filePath, updatedContent, 'utf8');
} catch (error) {
console.error(`Error updating references in ${filePath}:`, error);
}
}
const getDotStorybookReferences = async () => {
try {
// eslint-disable-next-line depend/ban-dependencies
const { $ } = await import('execa');
const { stdout } = await $`git grep -l \\.storybook`;
return stdout.split('\n').filter(Boolean);
} catch (error) {
return [];
}
};
export const rnstorybookConfig: Fix<Options> = {
id: 'rnstorybook-config',
versionRange: ['<9.0.0', '>=9.0.0'],
async check({ packageManager, mainConfigPath }) {
const allDependencies = await packageManager.getAllDependencies();
if (!allDependencies['@storybook/react-native']) {
return null;
}
// Check if .storybook directory exists
const projectDir = mainConfigPath ? join(mainConfigPath, '..', '..') : process.cwd();
const storybookDir = join(projectDir, '.storybook');
const rnStorybookDir = join(projectDir, '.rnstorybook');
// eslint-disable-next-line depend/ban-dependencies
const { globby } = await import('globby');
const requiresFiles = await globby(join(storybookDir, 'storybook.requires.*'));
if (existsSync(storybookDir) && requiresFiles.length > 0 && !existsSync(rnStorybookDir)) {
const dotStorybookReferences = await getDotStorybookReferences();
return { storybookDir, rnStorybookDir, dotStorybookReferences };
}
return null;
},
prompt({ dotStorybookReferences }) {
const references =
dotStorybookReferences.length > 0
? dedent`
We will update the following files to reference ${picocolors.yellow('.rnstorybook')}:
${dotStorybookReferences.map((ref: string) => picocolors.cyan('- ' + ref)).join('\n')}
`.trim()
: dedent`
Oddly, we did not find any source files that reference the ${picocolors.yellow('.storybook')} directory.
If they exist, please update them by hand to reference ${picocolors.yellow('.rnstorybook')} instead.
`.trim();
return dedent`
In Storybook 9, React Native projects use the ${picocolors.yellow('.rnstorybook')} directory for
configuration instead of ${picocolors.yellow('.storybook')}.
${references}
More info: ${picocolors.cyan('https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#react-native-config-dir-renamed')}
Would you like to automatically move your config files to the new location?`;
},
async run({ result: { storybookDir, rnStorybookDir, dotStorybookReferences }, dryRun }) {
if (!dryRun) {
await Promise.all(
dotStorybookReferences.map(async (ref) => {
await renameInFile(ref, '.storybook', '.rnstorybook');
})
);
await rename(storybookDir, rnStorybookDir);
}
},
};

View File

@ -6598,6 +6598,7 @@ __metadata:
create-storybook: "workspace:*"
cross-spawn: "npm:^7.0.3"
envinfo: "npm:^7.7.3"
execa: "npm:^9.5.2"
fd-package-json: "npm:^1.2.0"
find-up: "npm:^5.0.0"
giget: "npm:^1.0.0"