Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
66d541a
Initial plan
Copilot Apr 1, 2026
a889fb4
feat: add entry-point swapping, resolveEntryPoint, and websocket env …
Copilot Apr 1, 2026
537de98
fix: address code review feedback - use os.tmpdir() and reduce syscalls
Copilot Apr 1, 2026
2d4ed00
feat: split withStorybook into backwards-compat and entry-swap wrappers
Copilot Apr 1, 2026
4d7b128
fix: remove unnecessary re-exports from withStorybookSwap
Copilot Apr 1, 2026
d115749
feat: add unified withStorybook<T> wrapper at @storybook/react-native…
Copilot Apr 2, 2026
7e754f5
refactor: rename rspackConfig to bundlerConfig for clarity
Copilot Apr 2, 2026
a184687
refactor: restore metro/withStorybook.ts to original, move shared uti…
Copilot Apr 2, 2026
338b2e0
fix: remove ./metro/withStorybookSwap from public exports
Copilot Apr 2, 2026
8660d46
chore: add changeset for unified withStorybook wrapper (minor)
Copilot Apr 2, 2026
513cb9b
refactor: restructure withStorybook architecture per review feedback
Copilot Apr 2, 2026
b75df62
style: use top-level import for WebsocketsOptions in utils.ts
Copilot Apr 2, 2026
85ffd43
refactor: remove legacy baseWithStorybook dependency from enhanceMetr…
Copilot Apr 2, 2026
43edcd8
refactor: rewrite enhanceRepackConfig to implement entry-point swappi…
Copilot Apr 2, 2026
c9f413d
refactor: move shared setup (generate, createChannelServer) from enha…
Copilot Apr 2, 2026
2a65a27
refactor: merge swap into options argument for both enhancers
Copilot Apr 2, 2026
44dd17c
feat: add liteMode option to enhanceRepackConfig and improve environm…
ndelangen Apr 2, 2026
c3acbc0
feat: add TODO for liteMode support in enhanceRepackConfig
ndelangen Apr 2, 2026
597e02e
feat: implement liteMode support in enhanceRepackConfig
Copilot Apr 2, 2026
c861324
fix: remove unnecessary type casts in enhanceRepackConfig
Copilot Apr 2, 2026
969c088
remove enhanceMetroConfig and enhanceRepackConfig from tsup entrypoin…
Copilot Apr 7, 2026
4452d94
refactor: clean up code formatting and type casting in configuration …
ndelangen Apr 7, 2026
ce0fada
feat: add liteMode support to generate function and update related co…
ndelangen Apr 7, 2026
9b735ca
fix: update View instantiation to include options parameter
ndelangen Apr 7, 2026
986c3db
feat: enhance View component with SafeArea support and update generat…
ndelangen Apr 7, 2026
712ac17
test: remove liteMode tests from enhancers (liteMode moved to generat…
Copilot Apr 7, 2026
67a4f17
test: update generate.test.ts snapshots to match current generate.js …
Copilot Apr 7, 2026
36eb5be
feat: add `deviceAddons` property to separate on-device addons from c…
Copilot Apr 9, 2026
b87dffa
Merge branch 'next' into copilot/add-entry-point-swapping-storybook
ndelangen Apr 9, 2026
20bf4cb
refactor: improve logging and update liteMode handling in View and wi…
ndelangen Apr 9, 2026
db9ac2e
Merge branch 'copilot/add-entry-point-swapping-storybook' of github.c…
ndelangen Apr 9, 2026
a4d3348
make storage optional, never throw
ndelangen Apr 10, 2026
c14572f
feat: add WebSocket smoke server and environment variable support for…
ndelangen Apr 13, 2026
d586348
chore: update package.json files across multiple packages
ndelangen Apr 14, 2026
34221cc
refactor: deprecate `addons` field in Storybook configuration
ndelangen Apr 14, 2026
d6f40b6
fix: correct server condition in withStorybook function, see https://…
ndelangen Apr 14, 2026
5c4dc2b
style: format code for better readability in generate.js
ndelangen Apr 14, 2026
ed85eb8
fix: import StatusBar in View component for proper functionality
ndelangen Apr 16, 2026
cd869db
style: simplify logging condition in View component
ndelangen Apr 16, 2026
0d5f722
fix: update storage prop in View component for consistency
ndelangen Apr 16, 2026
c29d0eb
fix: ensure secured option defaults to false in withStorybook function
ndelangen Apr 16, 2026
ad34914
refactor: enhance generate function parameter structure and improve h…
ndelangen Apr 16, 2026
9f17632
refactor: improve WebSocket handling in withStorybook function
ndelangen Apr 16, 2026
fe68fd7
refactor: rename options parameter in generate function for clarity
ndelangen Apr 16, 2026
660fedf
style: fix formatting in package.json and withStorybook.ts
ndelangen Apr 16, 2026
d90278b
feat: add environment variable utility functions for better configura…
ndelangen Apr 16, 2026
97950c4
refactor: update generate function and WebSocket handling in withStor…
ndelangen Apr 16, 2026
688d5d9
feat: add disableUI option to enhance configuration flexibility
ndelangen Apr 16, 2026
c3d0b97
fix: check params.storage in warning condition to avoid unreachable code
Copilot Apr 16, 2026
9e6cbe3
fix: handle 'tty' and 'os' module resolution in enhanceMetroConfig
ndelangen Apr 16, 2026
c821d2e
Merge branch 'copilot/add-entry-point-swapping-storybook' of github.c…
ndelangen Apr 16, 2026
54a7cd7
sort package.json files
ndelangen Apr 16, 2026
04f84a4
Merge branch 'norbert/sort-package-json' into copilot/add-entry-point…
ndelangen Apr 16, 2026
1631731
Merge branch 'next' into copilot/add-entry-point-swapping-storybook
ndelangen Apr 16, 2026
11fba4e
delete
ndelangen Apr 16, 2026
e040a3f
fix: update deprecation warning for addons field in generate.js
ndelangen Apr 16, 2026
516ff63
fix: add missing newline at end of package.json files
ndelangen Apr 17, 2026
7cec271
fix: enhance withStorybook function to support options.enabled
ndelangen Apr 23, 2026
706b49a
Merge branch 'next' into copilot/add-entry-point-swapping-storybook
dannyhw Apr 24, 2026
412c3f6
refactor: streamline main config file loading in generate.js
ndelangen Apr 28, 2026
42f7dd5
chore: remove unused ws-smoke-server script and clean up package.json
ndelangen Apr 28, 2026
4030913
fix: handle optional options parameter in View constructor
ndelangen Apr 28, 2026
2c68fcb
feat: add liteMode option to enhanceMetroConfig and enhanceRepackConfig
ndelangen Apr 28, 2026
9e9f155
add example for testing new wrapper with expo
ndelangen Apr 29, 2026
419a6d9
fix: add missing newlines at the end of JSON files and clean up impor…
ndelangen Apr 29, 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
5 changes: 5 additions & 0 deletions .changeset/add-device-addons.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@storybook/react-native': minor
---

Add `deviceAddons` property to `StorybookConfig` for separating on-device addons from core addons. On-device addons listed in `deviceAddons` are only consumed at runtime by the code generator, not evaluated as presets by Storybook Core. This prevents `extract` failures caused by loading React Native code in a Node.js context. Backwards compatible: addons in the `addons` field continue to work.
5 changes: 5 additions & 0 deletions .changeset/unified-withstorybook-wrapper.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@storybook/react-native': minor
---

Add unified bundler-agnostic withStorybook wrapper at @storybook/react-native/withStorybook
6 changes: 3 additions & 3 deletions examples/expo-example/.rnstorybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ const main: StorybookConfig = {
files: '**/*.stories.?(ts|tsx|js|jsx)',
},
],
addons: [
deviceAddons: [
'storybook-addon-deep-controls',
'./local-addon-example',
{ name: '@storybook/addon-ondevice-controls' },
'@storybook/addon-ondevice-actions',
// '@storybook/addon-ondevice-backgrounds',
'@storybook/addon-ondevice-notes',
'storybook-addon-deep-controls',
'./local-addon-example',
],
reactNative: {
playFn: false,
Expand Down
7 changes: 4 additions & 3 deletions examples/expo-example/.rnstorybook/storybook.requires.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
/// <reference types="@storybook/react-native/metro-env" />
import { start, updateView, View, type Features } from '@storybook/react-native';


import "storybook-addon-deep-controls/register";
import "./local-addon-example/register";
import "@storybook/addon-ondevice-controls/register";
import "@storybook/addon-ondevice-actions/register";
import "@storybook/addon-ondevice-notes/register";
import "storybook-addon-deep-controls/register";
import "./local-addon-example/register";

const normalizedStories = [
{
Expand Down Expand Up @@ -64,7 +65,7 @@ const annotations = [

globalThis.STORIES = normalizedStories;
globalThis.STORYBOOK_WEBSOCKET = {
host: '192.168.1.171',
host: '192.168.86.21',
port: 7007,
secured: false,
};
Expand Down
13 changes: 13 additions & 0 deletions examples/expo-new-wrapper-example/.expo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
> Why do I have a folder named ".expo" in my project?
The ".expo" folder is created when an Expo project is started using "expo start" command.

> What do the files contain?
- "devices.json": contains information about devices that have recently opened this project. This is used to populate the "Development sessions" list in your development builds.
- "settings.json": contains the server configuration that is used to serve the application manifest.

> Should I commit the ".expo" folder?
No, you should not share the ".expo" folder. It does not contain any information that is relevant for other developers working on the project, it is specific to your machine.
Upon project creation, the ".expo" folder is already added to your ".gitignore" file.
3 changes: 3 additions & 0 deletions examples/expo-new-wrapper-example/.expo/devices.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"devices": []
}
18 changes: 18 additions & 0 deletions examples/expo-new-wrapper-example/.rnstorybook/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// This file is the app's bundle entry when Storybook is enabled.
// `withStorybook` swaps the resolver from the project's `index.js` to this
// file, so it must register a root component itself. See ../metro.config.js
// and ../README.md for the full picture.
import AsyncStorage from '@react-native-async-storage/async-storage';
import { registerRootComponent } from 'expo';

import { view } from './storybook.requires';

const StorybookUIRoot = view.getStorybookUI({
shouldPersistSelection: true,
storage: {
getItem: AsyncStorage.getItem,
setItem: AsyncStorage.setItem,
},
});

registerRootComponent(StorybookUIRoot);
12 changes: 12 additions & 0 deletions examples/expo-new-wrapper-example/.rnstorybook/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { StorybookConfig } from '@storybook/react-native';

const main: StorybookConfig = {
stories: ['../components/**/*.stories.?(ts|tsx|js|jsx)'],
deviceAddons: [
{ name: '@storybook/addon-ondevice-controls' },
'@storybook/addon-ondevice-actions',
],
framework: '@storybook/react-native',
};

export default main;
16 changes: 16 additions & 0 deletions examples/expo-new-wrapper-example/.rnstorybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { Preview } from '@storybook/react-native';

const preview: Preview = {
parameters: {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
layout: 'padded',
},
};

export default preview;
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/* 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';

const normalizedStories = [
{
titlePrefix: '',
directory: './components',
files: '**/*.stories.?(ts|tsx|js|jsx)',
importPathMatcher:
/^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?!\.)(?=.)[^/]*?\.stories\.(?:ts|tsx|js|jsx)?)$/,
req: require.context(
'../components',
true,
/^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?!\.)(?=.)[^/]*?\.stories\.(?:ts|tsx|js|jsx)?)$/
),
},
];

declare global {
var view: View;
var STORIES: typeof normalizedStories;
var STORYBOOK_WEBSOCKET: { host?: string; port?: number; secured?: boolean } | undefined;
var FEATURES: Features;
}

const annotations = [require('./preview'), require('@storybook/react-native/preview')];

globalThis.STORIES = normalizedStories;

module?.hot?.accept?.();

const options = {};

if (!globalThis.view) {
globalThis.view = start({
annotations,
storyEntries: normalizedStories,
options,
});
} else {
updateView(globalThis.view, annotations, normalizedStories, options);
}

export const view: View = globalThis.view;
17 changes: 17 additions & 0 deletions examples/expo-new-wrapper-example/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { SafeAreaView, Text, View } from 'react-native';

export default function App() {
return (
<SafeAreaView style={{ flex: 1 }}>
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', padding: 24 }}>
<Text style={{ fontSize: 18, fontWeight: '600', marginBottom: 8 }}>
Real app placeholder
</Text>
<Text style={{ textAlign: 'center', color: '#555' }}>
Storybook is disabled. Run `pnpm storybook` (sets EXPO_PUBLIC_STORYBOOK_ENABLED=true) to
launch the on-device Storybook UI instead.
</Text>
</View>
</SafeAreaView>
);
}
66 changes: 66 additions & 0 deletions examples/expo-new-wrapper-example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# expo-new-wrapper-example

Minimal Expo app that exercises the new universal `withStorybook` wrapper at
[`@storybook/react-native/withStorybook`](../../packages/react-native/src/withStorybook.ts).

It is intentionally separate from [`expo-example`](../expo-example) so the new
wrapper's behavior can be exercised in isolation, without rozenite, secured
websockets, EAS, or other extras getting in the way.

## How it works

The new wrapper performs an entry-point swap at the Metro resolver level. When
Storybook is enabled, Metro asks for the project entry (`index.js`) and the
resolver redirects it to `.rnstorybook/index.tsx`.

```mermaid
flowchart LR
subgraph enabled [pnpm storybook]
Enabled_indexJs[index.js] -->|"swap (resolver)"| SBIndex[.rnstorybook/index.tsx]
SBIndex --> RegSB["registerRootComponent(StorybookUIRoot)"]
end
subgraph disabled [pnpm start]
Disabled_indexJs[index.js] --> AppTsx[App.tsx]
AppTsx --> RegApp["registerRootComponent(App)"]
end
```

This means `.rnstorybook/index.tsx` becomes the bundle entry, so it is
responsible for calling `registerRootComponent` itself. There is no magic
shim — what you see is what gets executed.

When Storybook is disabled, the wrapper returns the Metro config unchanged.
`index.js` -> `App.tsx` runs as the real (placeholder) app, and Storybook is
not in the bundle because nothing imports it.

## Scripts

- `pnpm storybook` — sets `EXPO_PUBLIC_STORYBOOK_ENABLED=true` and starts Expo.
Metro swaps the entry to `.rnstorybook/index.tsx` and the on-device Storybook
UI is shown.
- `pnpm ios` / `pnpm android` — same as `pnpm storybook`, targeted at a
simulator.
- `pnpm start` — plain `expo start`, no env var. The placeholder `App.tsx`
renders.
- `pnpm check` — TypeScript check.

## Migration notes

Compared to the old `@storybook/react-native/metro/withStorybook`, two patterns
have changed in this example:

1. `.rnstorybook/index.tsx` is now a bootstrap entry. It must call
`registerRootComponent` (Expo) or `AppRegistry.registerComponent` (bare RN).
It does not need to default-export a component anymore.
2. `App.tsx` does not import `.rnstorybook`. Reaching Storybook is the
wrapper's job, not the app's. As a side benefit, `pnpm start` honestly
excludes Storybook from the bundle.

## What is not covered here

- Re.Pack — the universal wrapper's `enhanceRepackConfig` branch is not
exercised. A separate Re.Pack example is the natural follow-up.
- `expo-router` — `resolveEntryPoint`'s `expo-router/entry` detection is also
unexercised here. Add an `expo-router` variant if you want to cover it.
- The wider feature set demoed in `expo-example` (secured websockets, MCP,
rozenite, screenshot testing, web). Use `expo-example` for those.
19 changes: 19 additions & 0 deletions examples/expo-new-wrapper-example/app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "ExpoNewWrapperExample",
"slug": "expo-new-wrapper-example",
"version": "1.0.0",
"orientation": "portrait",
"scheme": "storybook-newwrapper",
"userInterfaceStyle": "automatic",
"newArchEnabled": true,
"ios": {
"bundleIdentifier": "com.storybook.newwrapperexample"
},
"android": {
"package": "com.storybook.newwrapperexample",
"edgeToEdgeEnabled": true
},
"experiments": {
"tsconfigPaths": true
}
}
7 changes: 7 additions & 0 deletions examples/expo-new-wrapper-example/babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = function (api) {
api.cache(true);
return {
presets: [['babel-preset-expo']],
plugins: ['react-native-worklets/plugin'],
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { Meta, StoryObj } from '@storybook/react-native';
import { fn } from 'storybook/test';

import { Button } from './Button';

const meta = {
component: Button,
args: {
onPress: fn(),
},
} satisfies Meta<typeof Button>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Default: Story = {
args: {
title: 'Press me',
},
};

export const Disabled: Story = {
args: {
title: 'Press me',
disabled: true,
},
};
25 changes: 25 additions & 0 deletions examples/expo-new-wrapper-example/components/Button/Button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Pressable, Text, type GestureResponderEvent } from 'react-native';

export interface ButtonProps {
title: string;
onPress?: (event: GestureResponderEvent) => void;
disabled?: boolean;
}

export function Button({ title, onPress, disabled }: ButtonProps) {
return (
<Pressable
onPress={onPress}
disabled={disabled}
style={({ pressed }) => ({
paddingVertical: 12,
paddingHorizontal: 20,
borderRadius: 8,
backgroundColor: disabled ? '#aaa' : pressed ? '#1d4ed8' : '#2563eb',
alignItems: 'center',
})}
>
<Text style={{ color: 'white', fontWeight: '600' }}>{title}</Text>
</Pressable>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { Meta, StoryObj } from '@storybook/react-native';

import { HelloText } from './HelloText';

const meta = {
component: HelloText,
} satisfies Meta<typeof HelloText>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Hello: Story = {
args: {
message: 'Hello, Storybook',
},
};

export const Goodbye: Story = {
args: {
message: 'Goodbye, Storybook',
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Text } from 'react-native';

export interface HelloTextProps {
message: string;
}

export function HelloText({ message }: HelloTextProps) {
return <Text style={{ fontSize: 18 }}>{message}</Text>;
}
5 changes: 5 additions & 0 deletions examples/expo-new-wrapper-example/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { registerRootComponent } from 'expo';

import App from './App';

registerRootComponent(App);
Loading
Loading