Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down
84 changes: 32 additions & 52 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,74 +24,54 @@ pnpm test:ci # Run tests in CI mode
pnpm lint # Run ESLint across the codebase
pnpm format:check # Check Prettier formatting
pnpm format:fix # Auto-fix Prettier formatting

# Documentation (from docs/ directory)
cd docs
pnpm start # Start development server
pnpm build # Build documentation
pnpm serve # Serve built documentation
```

## Architecture Overview
## On-Device Testing Tools

**pnpm workspaces monorepo** managed by Lerna containing React Native Storybook packages.
### agent-device (iOS/Android Simulator Control)

### Packages
Use `agent-device` to interact with iOS/Android simulators for testing the Storybook app:

**Apps**
```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
```

- examples/expo-example - Expo example app showcasing Storybook
- docs - Documentation site for Storybook React Native
After relaunching, you need to press the app in Expo Go to open it (e.g. click the "Expo Example" entry).

**Core:**
### rn-logs (React Native Log Streaming)

- `@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
Use `rn-logs` to stream console output from the running app:

**On-Device Addons:**
```bash
rn-logs apps # List running apps
rn-logs logs --app "host.exp.Exponent" # Stream logs from Expo Go
```

- `@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
Pipe through `grep` to filter: `rn-logs logs --app "host.exp.Exponent" 2>&1 | grep "KEYWORD"`

### Build System & Metro Configuration
## Architecture Overview

- Uses **tsup** for TypeScript compilation (ES2022, CommonJS output)
- Each package has its own `tsup.config.ts`
- `pnpm prepare` in a package builds it
**pnpm workspaces monorepo** managed by Lerna containing React Native Storybook packages.

The `withStorybook` Metro wrapper (for Metro-based projects):
### Key Concepts

- 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
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 (`storybook.requires.ts`)
4. **Portable stories** - Reuse stories in unit tests via `universal-test-renderer`
5. **fn() actions bridge** - `setupFnActionsBridge` in `storybook.requires.ts` bridges `fn()` mocks from `storybook/test` to the on-device actions panel. Must be called from user-land code (not package code) due to module identity — `storybook/test` uses a module-scoped listener Set.

The `StorybookPlugin` (for Re.Pack/Rspack/Webpack projects):
### Build System

- 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)
- Uses **tsup** for TypeScript compilation (ES2022, CommonJS output)
- Each package has its own `tsup.config.ts`
- `pnpm prepare` in a package builds it (or `pnpm -F @storybook/react-native prepare`)

### Testing

- Uses **jest** with `jest-expo` preset
- `universal-test-renderer` for portable story testing
- Story generation tested with Node's native test runner

### 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.)
- Uses **jest** with `jest-expo` preset for example app tests
- Story generation snapshots tested with Node's native test runner (`tests/scripts/`)
- Update Node test runner snapshots: `cd tests && node --test-update-snapshots --test scripts/generate.test.ts scripts/docgen.test.ts`
12 changes: 6 additions & 6 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
41 changes: 41 additions & 0 deletions examples/expo-example/.rnstorybook-nofactories/index.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<SafeAreaProvider>
<SafeAreaView style={{ flex: 1 }}>
<StatusBar hidden />
<View
style={{ flex: 1 }}
accessibilityLabel={story?.id}
testID={story?.id}
accessible
>
{children}
</View>
</SafeAreaView>
</SafeAreaProvider>
);
}
: isLiteUI
? LiteUI
: undefined,
});

export default StorybookUIRoot;
28 changes: 28 additions & 0 deletions examples/expo-example/.rnstorybook-nofactories/main.ts
Original file line number Diff line number Diff line change
@@ -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;
37 changes: 37 additions & 0 deletions examples/expo-example/.rnstorybook-nofactories/preview.tsx
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/* do not change this file, it is auto generated by storybook. */
/// <reference types="@storybook/react-native/metro-env" />
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;
8 changes: 3 additions & 5 deletions examples/expo-example/.rnstorybook/main.ts
Original file line number Diff line number Diff line change
@@ -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)',
Expand All @@ -26,6 +26,4 @@ const main: StorybookConfig = {
},

framework: '@storybook/react-native',
};

export default main;
});
18 changes: 5 additions & 13 deletions examples/expo-example/.rnstorybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -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: {
Expand All @@ -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
Expand All @@ -46,6 +40,4 @@ const preview: Preview = {
// 👇 Set the initial background color
backgrounds: { value: Appearance.getColorScheme() === 'dark' ? 'dark' : 'plain' },
},
};

export default preview;
});
7 changes: 6 additions & 1 deletion examples/expo-example/App.tsx
Original file line number Diff line number Diff line change
@@ -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;
Loading