diff --git a/.changeset/flat-impalas-press.md b/.changeset/flat-impalas-press.md new file mode 100644 index 0000000000..469572c3bd --- /dev/null +++ b/.changeset/flat-impalas-press.md @@ -0,0 +1,5 @@ +--- +'@storybook/react-native': patch +--- + +feat: adds csf next support diff --git a/.gitignore b/.gitignore index 3362676108..3c9e66a0a8 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,6 @@ dist .cache junit.xml coverage/ -*.lerna_backup build /**/LICENSE docs/public diff --git a/.prettierignore b/.prettierignore index eaf48a1e8f..3ea09995e2 100644 --- a/.prettierignore +++ b/.prettierignore @@ -3,6 +3,7 @@ pnpm-lock.yaml dist/ examples/expo-example/.expo examples/expo-example/.rnstorybook/storybook.requires.ts +examples/expo-example/.rnstorybook-nofactories/storybook.requires.ts docs/.docusaurus docs/build .claude/ diff --git a/CLAUDE.md b/CLAUDE.md index ee8cb8e3d3..ce02846438 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,97 +1,53 @@ This file provides guidance to agents when working with code in this repository. -Always check first if the react-native-best-practices skill can be used +check available mcp and skills -## Development Commands +use pnpm for commands, check package.json for scripts -```bash -# Initial Setup -pnpm install -pnpm build - -# Development -pnpm dev # Watch all packages for changes -pnpm example # Run the expo example app with Storybook - -# Story Generation -pnpm -F expo-example storybook-generate # Regenerate storybook.requires.ts +## On-Device Testing Tools -# Testing -pnpm test # Run unit tests across all packages -pnpm test:ci # Run tests in CI mode +- use agent-device to control a simulator `agent-device --help` +- use rn-logs to get metro logs `rn-logs logs --help` +- use the storybook mcp to select stories and get story list -# Code Quality -pnpm lint # Run ESLint across the codebase -pnpm format:check # Check Prettier formatting -pnpm format:fix # Auto-fix Prettier formatting +use curl to send events to channel server, such as to update the args: -# Documentation (from docs/ directory) -cd docs -pnpm start # Start development server -pnpm build # Build documentation -pnpm serve # Serve built documentation +```sh +curl -X POST http://localhost:7007/send-event \ + -H "Content-Type: application/json" \ + -d '{ + "type": "updateStoryArgs", + "args": [{ + "storyId": "controlexamples-controlexample--example", + "updatedArgs": { "name": "Alice", "age": 25 } + }] + }' ``` -## Architecture Overview - -**pnpm workspaces monorepo** managed by Lerna containing React Native Storybook packages. - -### Packages - -**Apps** - -- examples/expo-example - Expo example app showcasing Storybook -- docs - Documentation site for Storybook React Native - -**Core:** - -- `@storybook/react-native` - Main package providing Storybook functionality -- `@storybook/react-native-ui` - Full UI components for on-device Storybook -- `@storybook/react-native-ui-lite` - Lightweight UI components -- `@storybook/react-native-ui-common` - Shared UI components -- `@storybook/react-native-theming` - Theming utilities - -**On-Device Addons:** +### agent-device (iOS/Android Simulator Control) -- `@storybook/addon-ondevice-actions` - Log component interactions -- `@storybook/addon-ondevice-backgrounds` - Change story backgrounds -- `@storybook/addon-ondevice-controls` - Dynamically edit component props -- `@storybook/addon-ondevice-notes` - Add markdown documentation to stories - -### Build System & Metro Configuration - -- Uses **tsup** for TypeScript compilation (ES2022, CommonJS output) -- Each package has its own `tsup.config.ts` -- `pnpm prepare` in a package builds it - -The `withStorybook` Metro wrapper (for Metro-based projects): - -- Enables `unstable_allowRequireContext` for dynamic story imports -- Automatically generates `storybook.requires.ts` file -- Optional WebSocket server for remote control -- Can be conditionally enabled/disabled via `enabled` option -- Supports `liteMode` for reduced bundle size - -The `StorybookPlugin` (for Re.Pack/Rspack/Webpack projects): +```bash +agent-device open host.exp.Exponent --relaunch # Relaunch Expo Go +agent-device snapshot -c # Take accessibility snapshot (shows @refs) +agent-device click @e14 # Click element by ref from snapshot +agent-device find "Press me" click # Find text and click it +``` -- Alternative to `withStorybook` for non-Metro bundlers -- Imported from `@storybook/react-native/repack/withStorybook` -- Requires `enablePackageExports: true` in rspack resolve options -- Uses `DefinePlugin` for build-time `STORYBOOK_ENABLED` constant -- No `require.context` configuration needed (rspack handles it natively) -- Same options as `withStorybook` (enabled, configPath, useJs, docTools, liteMode, websockets) +After relaunching, you need to press the "Expo Example" to go to it. -### Testing +### rn-logs (React Native Log Streaming) -- Uses **jest** with `jest-expo` preset -- `universal-test-renderer` for portable story testing -- Story generation tested with Node's native test runner +```bash +rn-logs apps # List running apps +rn-logs logs --app "host.exp.Exponent" # Stream logs from Expo Go +``` -### Key Concepts +## Key Concepts 1. **CSF (Component Story Format)** - Standard story syntax 2. **On-device UI** - Native UI that runs directly on mobile devices -3. **Story requires generation** - Automatic generation of story imports via Metro -4. **Portable stories** - Reuse stories in unit tests -5. **WebSocket support** - Remote control stories from external devices -6. **Lite mode** - Alternative UI without heavy dependencies (reanimated, etc.) +3. **Story requires generation** - Automatic generation of story imports via Metro (`storybook.requires.ts`) +4. **Portable stories** - Reuse stories in unit tests via `universal-test-renderer` +5. web storybook codebase can be referenced and likely can be found at ../storybook (from root) + +additional information in docs folder and readme file diff --git a/ROADMAP.md b/ROADMAP.md index 7507a05a3f..4e09810ff0 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,12 +2,12 @@ For v11: -- csf factories -- backgrounds with globals -- addons into core -- make lite ui the default and remove dependencies from controls +- [x] csf factories +- [x] backgrounds with globals +- [ ] addons into core +- [ ] make lite ui the default and remove dependencies from controls stretch goals: -- simple docs implementation -- dev tooling like vscode extension and rn dev tools integration +- [ ] simple docs implementation +- [x] dev tooling like vscode extension and rn dev tools integration diff --git a/examples/expo-example/.rnstorybook-nofactories/index.tsx b/examples/expo-example/.rnstorybook-nofactories/index.tsx new file mode 100644 index 0000000000..be264b7cb0 --- /dev/null +++ b/examples/expo-example/.rnstorybook-nofactories/index.tsx @@ -0,0 +1,41 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { LiteUI } from '@storybook/react-native-ui-lite'; +import { StatusBar, View } from 'react-native'; +import { SafeAreaView, SafeAreaProvider } from 'react-native-safe-area-context'; +import { view } from './storybook.requires'; + +const isScreenshotTesting = process.env.EXPO_PUBLIC_SCREENSHOT_TESTING === 'true'; +const isLiteUI = process.env.EXPO_PUBLIC_LITE_UI === 'true'; + +const StorybookUIRoot = view.getStorybookUI({ + shouldPersistSelection: true, + storage: { + getItem: AsyncStorage.getItem, + setItem: AsyncStorage.setItem, + }, + enableWebsockets: true, + + CustomUIComponent: isScreenshotTesting + ? ({ children, story }) => { + return ( + + + + + ); + } + : isLiteUI + ? LiteUI + : undefined, +}); + +export default StorybookUIRoot; diff --git a/examples/expo-example/.rnstorybook-nofactories/main.ts b/examples/expo-example/.rnstorybook-nofactories/main.ts new file mode 100644 index 0000000000..d524e50c97 --- /dev/null +++ b/examples/expo-example/.rnstorybook-nofactories/main.ts @@ -0,0 +1,28 @@ +import type { StorybookConfig } from '@storybook/react-native'; + +const main: StorybookConfig = { + stories: [ + '../components/**/!(*.factories).stories.?(ts|tsx|js|jsx)', + '../other_components/**/!(*.factories).stories.?(ts|tsx|js|jsx)', + { + directory: '../../../packages/react-native-ui', + titlePrefix: 'react-native-ui', + files: '**/!(*.factories).stories.?(ts|tsx|js|jsx)', + }, + ], + addons: [ + '@storybook/addon-ondevice-controls', + '@storybook/addon-ondevice-actions', + '@storybook/addon-ondevice-notes', + 'storybook-addon-deep-controls', + ], + reactNative: { + playFn: false, + }, + features: { + ondeviceBackgrounds: true, + }, + framework: '@storybook/react-native', +}; + +export default main; diff --git a/examples/expo-example/.rnstorybook-nofactories/preview.tsx b/examples/expo-example/.rnstorybook-nofactories/preview.tsx new file mode 100644 index 0000000000..37d6c0ccaf --- /dev/null +++ b/examples/expo-example/.rnstorybook-nofactories/preview.tsx @@ -0,0 +1,37 @@ +import { Appearance } from 'react-native'; + +const preview = { + parameters: { + actions: { argTypesRegex: '^on[A-Z].*' }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/, + }, + }, + options: { + storySort: { + method: 'alphabetical' as const, + includeNames: true, + order: ['ControlExamples', ['ControlExample'], 'InteractionExample', 'DeepControls'], + }, + }, + hideFullScreenButton: false, + noSafeArea: false, + my_param: 'anything', + layout: 'padded', + storybookUIVisibility: 'visible', + backgrounds: { + options: { + dark: { name: 'dark', value: '#333' }, + light: { name: 'plain', value: '#fff' }, + app: { name: 'app', value: '#eeeeee' }, + }, + }, + }, + initialGlobals: { + backgrounds: { value: Appearance.getColorScheme() === 'dark' ? 'dark' : 'plain' }, + }, +}; + +export default preview; diff --git a/examples/expo-example/.rnstorybook-nofactories/storybook.requires.ts b/examples/expo-example/.rnstorybook-nofactories/storybook.requires.ts new file mode 100644 index 0000000000..19fd2c4f1c --- /dev/null +++ b/examples/expo-example/.rnstorybook-nofactories/storybook.requires.ts @@ -0,0 +1,82 @@ +/* do not change this file, it is auto generated by storybook. */ +/// +import { start, updateView, View, type Features } from '@storybook/react-native'; + +import "@storybook/addon-ondevice-controls/register"; +import "@storybook/addon-ondevice-actions/register"; +import "@storybook/addon-ondevice-notes/register"; +import "storybook-addon-deep-controls/register"; + +const normalizedStories = [ + { + titlePrefix: "", + directory: "./components", + files: "**/!(*.factories).stories.?(ts|tsx|js|jsx)", + importPathMatcher: /^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?:(?!(?:[^/]*?\.factories))[^/]*?)\.stories\.(?:ts|tsx|js|jsx)?)$/, + req: require.context( + '../components', + true, + /^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?:(?!(?:[^/]*?\.factories))[^/]*?)\.stories\.(?:ts|tsx|js|jsx)?)$/ + ), + }, + { + titlePrefix: "", + directory: "./other_components", + files: "**/!(*.factories).stories.?(ts|tsx|js|jsx)", + importPathMatcher: /^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?:(?!(?:[^/]*?\.factories))[^/]*?)\.stories\.(?:ts|tsx|js|jsx)?)$/, + req: require.context( + '../other_components', + true, + /^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?:(?!(?:[^/]*?\.factories))[^/]*?)\.stories\.(?:ts|tsx|js|jsx)?)$/ + ), + }, + { + titlePrefix: "react-native-ui", + directory: "../../packages/react-native-ui", + files: "**/!(*.factories).stories.?(ts|tsx|js|jsx)", + importPathMatcher: /^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?:(?!(?:[^/]*?\.factories))[^/]*?)\.stories\.(?:ts|tsx|js|jsx)?)$/, + req: require.context( + '../../../packages/react-native-ui', + true, + /^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?:(?!(?:[^/]*?\.factories))[^/]*?)\.stories\.(?:ts|tsx|js|jsx)?)$/ + ), + } +]; + + +declare global { + var view: View; + var STORIES: typeof normalizedStories; + var STORYBOOK_WEBSOCKET: { host: string; port: number } | undefined; + var FEATURES: Features; +} + + +const annotations = [ + require('./preview'), + require("@storybook/react-native/preview"), + require('storybook-addon-deep-controls/preview') +]; + +globalThis.STORIES = normalizedStories; +globalThis.STORYBOOK_WEBSOCKET = { host: '192.168.1.172', port: 7007 }; + +module?.hot?.accept?.(); + +globalThis.FEATURES.ondeviceBackgrounds = true; + +const options = { + "playFn": false +} + +if (!globalThis.view) { + globalThis.view = start({ + annotations, + storyEntries: normalizedStories, + options, + }); +} else { + updateView(globalThis.view, annotations, normalizedStories, options); +} + +export const view: View = globalThis.view; diff --git a/examples/expo-example/.rnstorybook/main.ts b/examples/expo-example/.rnstorybook/main.ts index 7b80aa3ef8..0038c181a7 100644 --- a/examples/expo-example/.rnstorybook/main.ts +++ b/examples/expo-example/.rnstorybook/main.ts @@ -1,6 +1,6 @@ -import type { StorybookConfig } from '@storybook/react-native'; +import { defineMain } from '@storybook/react-native/node'; -const main: StorybookConfig = { +export default defineMain({ stories: [ '../components/**/*.stories.?(ts|tsx|js|jsx)', '../other_components/**/*.stories.?(ts|tsx|js|jsx)', @@ -26,6 +26,4 @@ const main: StorybookConfig = { }, framework: '@storybook/react-native', -}; - -export default main; +}); diff --git a/examples/expo-example/.rnstorybook/preview.tsx b/examples/expo-example/.rnstorybook/preview.tsx index 1bdcddad87..39e7955761 100644 --- a/examples/expo-example/.rnstorybook/preview.tsx +++ b/examples/expo-example/.rnstorybook/preview.tsx @@ -1,9 +1,11 @@ import { Appearance } from 'react-native'; -import type { Preview } from '@storybook/react-native'; // import { withBackgrounds } from '@storybook/addon-ondevice-backgrounds'; +import { definePreview } from '@storybook/react-native'; -const preview: Preview = { +export default definePreview({ + addons: [], // decorators: [withBackgrounds], + parameters: { actions: { argTypesRegex: '^on[A-Z].*' }, controls: { @@ -24,14 +26,6 @@ const preview: Preview = { my_param: 'anything', layout: 'padded', // fullscreen, centered, padded storybookUIVisibility: 'visible', // visible, hidden - // backgrounds: { - // default: Appearance.getColorScheme() === 'dark' ? 'dark' : 'plain', - // values: [ - // { name: 'plain', value: 'white' }, - // { name: 'dark', value: '#333' }, - // { name: 'app', value: '#eeeeee' }, - // ], - // }, backgrounds: { options: { // 👇 Default options @@ -46,6 +40,4 @@ const preview: Preview = { // 👇 Set the initial background color backgrounds: { value: Appearance.getColorScheme() === 'dark' ? 'dark' : 'plain' }, }, -}; - -export default preview; +}); diff --git a/examples/expo-example/App.tsx b/examples/expo-example/App.tsx index 9af8bc40d6..bb01807ff0 100644 --- a/examples/expo-example/App.tsx +++ b/examples/expo-example/App.tsx @@ -1,4 +1,9 @@ // fixes fast refresh on web import '@expo/metro-runtime'; -export { default } from './.rnstorybook'; +const App = + process.env.EXPO_PUBLIC_NO_FACTORIES === 'true' + ? require('./.rnstorybook-nofactories').default + : require('./.rnstorybook').default; + +export default App; diff --git a/examples/expo-example/components/ActionExample/Actions.factories.stories.tsx b/examples/expo-example/components/ActionExample/Actions.factories.stories.tsx new file mode 100644 index 0000000000..8f303a4dd2 --- /dev/null +++ b/examples/expo-example/components/ActionExample/Actions.factories.stories.tsx @@ -0,0 +1,31 @@ +import { ActionButton } from './Actions'; +import { fn } from 'storybook/test'; +import preview from '../../.rnstorybook/preview'; + +const meta = preview.meta({ + component: ActionButton, + parameters: { + notes: ` +# Button + +This is a button component. +You use it like this: + +\`\`\`tsx +