diff --git a/.changeset/add-device-addons.md b/.changeset/add-device-addons.md
new file mode 100644
index 0000000000..1d447975af
--- /dev/null
+++ b/.changeset/add-device-addons.md
@@ -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.
diff --git a/.changeset/unified-withstorybook-wrapper.md b/.changeset/unified-withstorybook-wrapper.md
new file mode 100644
index 0000000000..c441f3c709
--- /dev/null
+++ b/.changeset/unified-withstorybook-wrapper.md
@@ -0,0 +1,5 @@
+---
+'@storybook/react-native': minor
+---
+
+Add unified bundler-agnostic withStorybook wrapper at @storybook/react-native/withStorybook
diff --git a/examples/expo-example/.rnstorybook/main.ts b/examples/expo-example/.rnstorybook/main.ts
index 7b80aa3ef8..6a3881e71a 100644
--- a/examples/expo-example/.rnstorybook/main.ts
+++ b/examples/expo-example/.rnstorybook/main.ts
@@ -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,
diff --git a/examples/expo-example/.rnstorybook/storybook.requires.ts b/examples/expo-example/.rnstorybook/storybook.requires.ts
index 8db95661a6..3af3167251 100644
--- a/examples/expo-example/.rnstorybook/storybook.requires.ts
+++ b/examples/expo-example/.rnstorybook/storybook.requires.ts
@@ -2,11 +2,12 @@
///
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 = [
{
@@ -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,
};
diff --git a/examples/expo-new-wrapper-example/.expo/README.md b/examples/expo-new-wrapper-example/.expo/README.md
new file mode 100644
index 0000000000..ce8c4b6f60
--- /dev/null
+++ b/examples/expo-new-wrapper-example/.expo/README.md
@@ -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.
diff --git a/examples/expo-new-wrapper-example/.expo/devices.json b/examples/expo-new-wrapper-example/.expo/devices.json
new file mode 100644
index 0000000000..5efff6c8cb
--- /dev/null
+++ b/examples/expo-new-wrapper-example/.expo/devices.json
@@ -0,0 +1,3 @@
+{
+ "devices": []
+}
diff --git a/examples/expo-new-wrapper-example/.rnstorybook/index.tsx b/examples/expo-new-wrapper-example/.rnstorybook/index.tsx
new file mode 100644
index 0000000000..0ade757c24
--- /dev/null
+++ b/examples/expo-new-wrapper-example/.rnstorybook/index.tsx
@@ -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);
diff --git a/examples/expo-new-wrapper-example/.rnstorybook/main.ts b/examples/expo-new-wrapper-example/.rnstorybook/main.ts
new file mode 100644
index 0000000000..f68a77a48e
--- /dev/null
+++ b/examples/expo-new-wrapper-example/.rnstorybook/main.ts
@@ -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;
diff --git a/examples/expo-new-wrapper-example/.rnstorybook/preview.tsx b/examples/expo-new-wrapper-example/.rnstorybook/preview.tsx
new file mode 100644
index 0000000000..e23acedf92
--- /dev/null
+++ b/examples/expo-new-wrapper-example/.rnstorybook/preview.tsx
@@ -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;
diff --git a/examples/expo-new-wrapper-example/.rnstorybook/storybook.requires.ts b/examples/expo-new-wrapper-example/.rnstorybook/storybook.requires.ts
new file mode 100644
index 0000000000..9251df34cf
--- /dev/null
+++ b/examples/expo-new-wrapper-example/.rnstorybook/storybook.requires.ts
@@ -0,0 +1,48 @@
+/* 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';
+
+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;
diff --git a/examples/expo-new-wrapper-example/App.tsx b/examples/expo-new-wrapper-example/App.tsx
new file mode 100644
index 0000000000..2bcdc44a28
--- /dev/null
+++ b/examples/expo-new-wrapper-example/App.tsx
@@ -0,0 +1,17 @@
+import { SafeAreaView, Text, View } from 'react-native';
+
+export default function App() {
+ return (
+
+
+
+ Real app placeholder
+
+
+ Storybook is disabled. Run `pnpm storybook` (sets EXPO_PUBLIC_STORYBOOK_ENABLED=true) to
+ launch the on-device Storybook UI instead.
+
+
+
+ );
+}
diff --git a/examples/expo-new-wrapper-example/README.md b/examples/expo-new-wrapper-example/README.md
new file mode 100644
index 0000000000..c8555cad0b
--- /dev/null
+++ b/examples/expo-new-wrapper-example/README.md
@@ -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.
diff --git a/examples/expo-new-wrapper-example/app.json b/examples/expo-new-wrapper-example/app.json
new file mode 100644
index 0000000000..e16ad4de62
--- /dev/null
+++ b/examples/expo-new-wrapper-example/app.json
@@ -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
+ }
+}
diff --git a/examples/expo-new-wrapper-example/babel.config.js b/examples/expo-new-wrapper-example/babel.config.js
new file mode 100644
index 0000000000..6f556e8b74
--- /dev/null
+++ b/examples/expo-new-wrapper-example/babel.config.js
@@ -0,0 +1,7 @@
+module.exports = function (api) {
+ api.cache(true);
+ return {
+ presets: [['babel-preset-expo']],
+ plugins: ['react-native-worklets/plugin'],
+ };
+};
diff --git a/examples/expo-new-wrapper-example/components/Button/Button.stories.tsx b/examples/expo-new-wrapper-example/components/Button/Button.stories.tsx
new file mode 100644
index 0000000000..f6ea2df854
--- /dev/null
+++ b/examples/expo-new-wrapper-example/components/Button/Button.stories.tsx
@@ -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;
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {
+ args: {
+ title: 'Press me',
+ },
+};
+
+export const Disabled: Story = {
+ args: {
+ title: 'Press me',
+ disabled: true,
+ },
+};
diff --git a/examples/expo-new-wrapper-example/components/Button/Button.tsx b/examples/expo-new-wrapper-example/components/Button/Button.tsx
new file mode 100644
index 0000000000..8a3c9dc13b
--- /dev/null
+++ b/examples/expo-new-wrapper-example/components/Button/Button.tsx
@@ -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 (
+ ({
+ paddingVertical: 12,
+ paddingHorizontal: 20,
+ borderRadius: 8,
+ backgroundColor: disabled ? '#aaa' : pressed ? '#1d4ed8' : '#2563eb',
+ alignItems: 'center',
+ })}
+ >
+ {title}
+
+ );
+}
diff --git a/examples/expo-new-wrapper-example/components/HelloText/HelloText.stories.tsx b/examples/expo-new-wrapper-example/components/HelloText/HelloText.stories.tsx
new file mode 100644
index 0000000000..181032182e
--- /dev/null
+++ b/examples/expo-new-wrapper-example/components/HelloText/HelloText.stories.tsx
@@ -0,0 +1,22 @@
+import type { Meta, StoryObj } from '@storybook/react-native';
+
+import { HelloText } from './HelloText';
+
+const meta = {
+ component: HelloText,
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Hello: Story = {
+ args: {
+ message: 'Hello, Storybook',
+ },
+};
+
+export const Goodbye: Story = {
+ args: {
+ message: 'Goodbye, Storybook',
+ },
+};
diff --git a/examples/expo-new-wrapper-example/components/HelloText/HelloText.tsx b/examples/expo-new-wrapper-example/components/HelloText/HelloText.tsx
new file mode 100644
index 0000000000..b2c854cc68
--- /dev/null
+++ b/examples/expo-new-wrapper-example/components/HelloText/HelloText.tsx
@@ -0,0 +1,9 @@
+import { Text } from 'react-native';
+
+export interface HelloTextProps {
+ message: string;
+}
+
+export function HelloText({ message }: HelloTextProps) {
+ return {message};
+}
diff --git a/examples/expo-new-wrapper-example/index.js b/examples/expo-new-wrapper-example/index.js
new file mode 100644
index 0000000000..ce8f2073fb
--- /dev/null
+++ b/examples/expo-new-wrapper-example/index.js
@@ -0,0 +1,5 @@
+import { registerRootComponent } from 'expo';
+
+import App from './App';
+
+registerRootComponent(App);
diff --git a/examples/expo-new-wrapper-example/metro.config.js b/examples/expo-new-wrapper-example/metro.config.js
new file mode 100644
index 0000000000..3299e4d493
--- /dev/null
+++ b/examples/expo-new-wrapper-example/metro.config.js
@@ -0,0 +1,25 @@
+// Learn more https://docs.expo.io/guides/customizing-metro
+const { getDefaultConfig } = require('expo/metro-config');
+const path = require('path');
+
+const projectRoot = __dirname;
+const workspaceRoot = path.resolve(projectRoot, '../../');
+
+/**
+ * Metro configuration
+ * https://reactnative.dev/docs/metro
+ *
+ * @type {import('metro-config').MetroConfig}
+ */
+const defaultConfig = getDefaultConfig(projectRoot);
+
+defaultConfig.watchFolders = [workspaceRoot];
+
+defaultConfig.resolver.nodeModulesPaths = [
+ path.resolve(projectRoot, 'node_modules'),
+ path.resolve(workspaceRoot, 'node_modules'),
+];
+
+const { withStorybook } = require('@storybook/react-native/withStorybook');
+
+module.exports = withStorybook(defaultConfig, {});
diff --git a/examples/expo-new-wrapper-example/package.json b/examples/expo-new-wrapper-example/package.json
new file mode 100644
index 0000000000..671a1fca44
--- /dev/null
+++ b/examples/expo-new-wrapper-example/package.json
@@ -0,0 +1,61 @@
+{
+ "name": "expo-new-wrapper-example",
+ "version": "0.0.0",
+ "private": true,
+ "main": "index.js",
+ "scripts": {
+ "start": "expo start",
+ "ios": "expo start --ios",
+ "android": "expo start --android",
+ "storybook:ios": "STORYBOOK_ENABLED=true expo start --ios",
+ "storybook:android": "STORYBOOK_ENABLED=true expo start --android",
+ "check": "tsc --noEmit"
+ },
+ "dependencies": {
+ "@expo/metro-runtime": "~55.0.7",
+ "@gorhom/bottom-sheet": "^5.2.8",
+ "@react-native-async-storage/async-storage": "2.2.0",
+ "@react-native-community/datetimepicker": "8.6.0",
+ "@react-native-community/slider": "5.1.2",
+ "@storybook/addon-ondevice-actions": "^10.3.2",
+ "@storybook/addon-ondevice-backgrounds": "^10.3.2",
+ "@storybook/addon-ondevice-controls": "^10.3.2",
+ "@storybook/addon-ondevice-notes": "^10.3.2",
+ "@storybook/addon-react-native-server": "^1.0.1",
+ "@storybook/react": "^10.3.2",
+ "@storybook/react-native": "^10.3.2",
+ "@storybook/react-native-ui-lite": "^10.3.2",
+ "@storybook/react-native-web-vite": "^10.3.2",
+ "babel-plugin-react-compiler": "^1.0.0",
+ "expo": "^55.0.14",
+ "expo-updates": "~55.0.16",
+ "react": "19.2.0",
+ "react-compiler-runtime": "^1.0.0",
+ "react-dom": "19.2.0",
+ "react-native": "0.83.4",
+ "react-native-gesture-handler": "~2.30.0",
+ "react-native-reanimated": "~4.2.1",
+ "react-native-safe-area-context": "^5",
+ "react-native-svg": "15.15.3",
+ "react-native-web": "^0.21.2",
+ "react-native-worklets": "0.7.2",
+ "storybook": "^10.3.2",
+ "storybook-addon-deep-controls": "^0.10.0",
+ "ws": "^8.20.0"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.26.0",
+ "@dannyhw/rozenite-storybook": "0.0.2",
+ "@rozenite/metro": "^1.6.0",
+ "@testing-library/react-native": "14.0.0-beta.0",
+ "@types/react": "~19.2.14",
+ "@types/ws": "^8.18.1",
+ "babel-plugin-react-docgen-typescript": "^1.5.1",
+ "expo-atlas": "^0.4.3",
+ "jest": "^29.7.0",
+ "jest-expo": "~55.0.11",
+ "test-renderer": "^0.15.0",
+ "typescript": "~5.9.3",
+ "vite": "^8.0.5"
+ }
+}
diff --git a/examples/expo-new-wrapper-example/tsconfig.json b/examples/expo-new-wrapper-example/tsconfig.json
new file mode 100644
index 0000000000..f500ad9fc5
--- /dev/null
+++ b/examples/expo-new-wrapper-example/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "compilerOptions": {
+ "skipLibCheck": true,
+ "baseUrl": "./",
+ "strict": true,
+ "esModuleInterop": true
+ },
+ "extends": "expo/tsconfig.base",
+ "include": [".rnstorybook/**/*", "./*"]
+}
diff --git a/package.json b/package.json
index 1aeb0cf321..e5a24d9ca2 100644
--- a/package.json
+++ b/package.json
@@ -29,10 +29,11 @@
"dev": "pnpm -r --parallel run dev",
"example": "pnpm --filter expo-example storybook",
"example:lite": "pnpm --filter expo-example storybook:lite",
+ "example-new-wrapper": "pnpm --filter expo-new-wrapper-example storybook",
"format:check": "prettier --check --experimental-cli .",
"format:fix": "prettier --write --experimental-cli .",
"lint": "eslint --cache -c ./eslint.config.js",
- "lint:fix": "lint --fix",
+ "lint:fix": "pnpm lint --fix",
"publish:canary": "pnpm changeset publish --tag canary",
"repo:fix": "sherif --fix -r unsync-similar-dependencies",
"repo:lint": "sherif -r unsync-similar-dependencies",
diff --git a/packages/react-native/package.json b/packages/react-native/package.json
index 53d794deb8..b1db714d45 100644
--- a/packages/react-native/package.json
+++ b/packages/react-native/package.json
@@ -19,6 +19,7 @@
"license": "MIT",
"exports": {
".": "./dist/index.js",
+ "./withStorybook": "./dist/withStorybook.js",
"./metro/withStorybook": "./dist/metro/withStorybook.js",
"./repack/withStorybook": "./dist/repack/withStorybook.js",
"./metro-env": "./metro-env.d.ts",
diff --git a/packages/react-native/scripts/generate.js b/packages/react-native/scripts/generate.js
index 1c94902bb1..d52b18c0c3 100644
--- a/packages/react-native/scripts/generate.js
+++ b/packages/react-native/scripts/generate.js
@@ -5,7 +5,12 @@ const {
resolveAddonFile,
getAddonName,
} = require('./common');
-const { normalizeStories, globToRegexp, loadMainConfig } = require('storybook/internal/common');
+const {
+ normalizeStories,
+ globToRegexp,
+ loadMainConfig,
+ getInterpretedFile,
+} = require('storybook/internal/common');
const { interopRequireDefault } = require('./require-interop');
const fs = require('fs');
const { networkInterfaces } = require('node:os');
@@ -14,6 +19,33 @@ const path = require('path');
const cwd = process.cwd();
+const MAIN_ADDONS_DEPRECATION_URL =
+ 'https://github.com/storybookjs/react-native/blob/main/MIGRATION.md#deprecating-addons-in-rnstorybook-main';
+
+/**
+ * @param {{ addons?: unknown[] }} main
+ * @param {string} configPath
+ *
+ * @todo Remove support for `main.addons` in a future major version.
+ */
+function warnDeprecatedMainAddonsField(main, configPath) {
+ const addons = main.addons ?? [];
+ if (addons.length === 0) {
+ return;
+ }
+
+ const names = addons
+ .map((addon) => getAddonName(addon))
+ .filter((name) => typeof name === 'string');
+ const list = [...new Set(names)].join(', ');
+ console.warn(
+ `[Storybook React Native] The \`addons\` field in your main config (${configPath}) is deprecated and will be removed in a future major version.\n` +
+ `Move every entry to \`deviceAddons\` instead. That includes on-device UI packages (\`@storybook/addon-ondevice-*\`), other addons you bundle with the app (for example storybook-addon-deep-controls), and local paths such as ./my-addon.\n` +
+ (list ? `Still listed under \`addons\`: ${list}.\n` : '') +
+ `Details: ${MAIN_ADDONS_DEPRECATION_URL}`
+ );
+}
+
const loadMain = async ({ configPath, cwd }) => {
try {
const main = await loadMainConfig({ configDir: configPath, cwd });
@@ -22,15 +54,11 @@ const loadMain = async ({ configPath, cwd }) => {
console.error('Error loading main config, trying fallback');
}
- const mainPathTs = path.resolve(cwd, configPath, `main.ts`);
- const mainPathJs = path.resolve(cwd, configPath, `main.js`);
- if (fs.existsSync(mainPathTs)) {
- return interopRequireDefault(mainPathTs);
- } else if (fs.existsSync(mainPathJs)) {
- return interopRequireDefault(mainPathJs);
- } else {
- throw new Error(`Main config file not found at ${mainPathTs} or ${mainPathJs}`);
+ const mainPath = getInterpretedFile(path.resolve(cwd, configPath, 'main'));
+ if (!mainPath) {
+ throw new Error(`Main config file not found in ${path.resolve(cwd, configPath)}`);
}
+ return interopRequireDefault(mainPath);
};
/**
@@ -50,14 +78,27 @@ function getLocalIPAddress() {
return '0.0.0.0';
}
-async function generate({
- configPath,
- useJs = false,
- docTools = true,
- host = undefined,
- port = undefined,
- secured = false,
-}) {
+/**
+ * @param {{
+ * configPath: string;
+ * useJs?: boolean;
+ * docTools?: boolean;
+ * host?: string;
+ * port?: number;
+ * secured?: boolean;
+ * disableUI?: boolean;
+ * }} generateOptions
+ */
+async function generate(generateOptions) {
+ const {
+ configPath,
+ useJs = false,
+ docTools = true,
+ host = undefined,
+ port = undefined,
+ secured = false,
+ disableUI = false,
+ } = generateOptions;
// here we want to get the ip address and pass it to rn storybook so that devices can connect over lan easily
const channelHost = host === 'auto' ? getLocalIPAddress() : host;
const storybookRequiresLocation = path.resolve(
@@ -68,6 +109,8 @@ async function generate({
const main = await loadMain({ configPath, cwd });
+ warnDeprecatedMainAddonsField(main, configPath);
+
const storiesSpecifiers = normalizeStories(main.stories, {
configDir: configPath,
workingDir: cwd,
@@ -95,7 +138,12 @@ async function generate({
const registeredAddons = [];
- for (const addon of main.addons) {
+ const allAddons = [
+ ...(main.addons ?? []), // TODO remove in v11
+ ...(main.deviceAddons ?? []),
+ ];
+
+ for (const addon of allAddons) {
const registerPath = resolveAddonFile(
getAddonName(addon),
'register',
@@ -116,7 +164,7 @@ async function generate({
enhancers.push(docToolsAnnotation);
}
- for (const addon of main.addons) {
+ for (const addon of allAddons) {
const previewPath = resolveAddonFile(
getAddonName(addon),
'preview',
@@ -132,7 +180,11 @@ async function generate({
let options = '';
let optionsVar = '';
- const reactNativeOptions = main.reactNative;
+ const reactNativeOptions = main.reactNative ?? {};
+
+ if (disableUI) {
+ reactNativeOptions.disableUI = true;
+ }
if (reactNativeOptions && typeof reactNativeOptions === 'object') {
optionsVar = `const options = ${JSON.stringify(reactNativeOptions, null, 2)}`;
@@ -192,6 +244,7 @@ declare global {
const fileContent = `/* do not change this file, it is auto generated by storybook. */
${useJs ? '' : '/// \n'}import { start, updateView${useJs ? '' : ', View, type Features'} } from '@storybook/react-native';
+
${registeredAddons.join('\n')}
const normalizedStories = [
diff --git a/packages/react-native/src/Start.tsx b/packages/react-native/src/Start.tsx
index c186b5a308..47e0f255e8 100644
--- a/packages/react-native/src/Start.tsx
+++ b/packages/react-native/src/Start.tsx
@@ -143,7 +143,7 @@ export function start({
previewView as any
);
- const view = new View(preview, channel);
+ const view = new View(preview, channel, options);
if (global) {
global.__STORYBOOK_ADDONS_CHANNEL__ = channel;
diff --git a/packages/react-native/src/View.tsx b/packages/react-native/src/View.tsx
index b0f75610b3..b9ed70c39f 100644
--- a/packages/react-native/src/View.tsx
+++ b/packages/react-native/src/View.tsx
@@ -11,7 +11,10 @@ import dedent from 'dedent';
import { patchChannelForRN } from './patchChannelForRN';
import deepmerge from 'deepmerge';
import { useEffect, useMemo, useReducer, useState } from 'react';
+import { SafeAreaView, SafeAreaProvider } from 'react-native-safe-area-context';
+
import {
+ StatusBar,
ActivityIndicator,
Linking,
Platform,
@@ -118,11 +121,13 @@ export class View {
_webUrl: string;
_storage: Storage;
_channel: Channel;
+ _options: any;
_idToPrepared: Record> = {};
- constructor(preview: PreviewWithSelection, channel: Channel) {
+ constructor(preview: PreviewWithSelection, channel: Channel, options: any) {
this._preview = preview;
this._channel = channel;
+ this._options = options ?? {};
}
_storyIdExists = (storyId: string) => {
@@ -201,13 +206,9 @@ export class View {
_getServerChannel = (params: Partial = {}) => {
const host = this._getHost(params);
-
const port = `:${this.__getPort(params)}`;
-
const query = params.query || '';
-
const websocketType = this._isSecureConnection(params) ? 'wss' : 'ws';
-
const url = `${websocketType}://${host}${port}/${query}`;
const channel = new Channel({
@@ -236,14 +237,21 @@ export class View {
getStorybookUI = (params: Partial = {}) => {
const {
- shouldPersistSelection = true,
- onDeviceUI = true,
enableWebsockets = false,
- storage,
CustomUIComponent,
hasStoryWrapper: storyViewWrapper = true,
} = params;
+ const storage = params.storage ?? {
+ getItem: async (key) => null,
+ setItem: async (key, value) => {},
+ };
+
+ const onDeviceUI = this._options?.disableUI ? false : (params.onDeviceUI ?? true);
+ const shouldPersistSelection = this._options.disableUI
+ ? false
+ : (params.shouldPersistSelection ?? true);
+
const getFullUI = (enabled: boolean): SBUI => {
if (enabled) {
try {
@@ -260,7 +268,10 @@ export class View {
const FullUI: SBUI = getFullUI(onDeviceUI && !CustomUIComponent);
- this._storage = storage;
+ this._storage = storage ?? {
+ getItem: async (key) => null,
+ setItem: async (key, value) => {},
+ };
const initialStory = this._getInitialStory(params);
@@ -391,7 +402,7 @@ export class View {
self._setStory = (newStory: StoryContext) => {
setContext(newStory);
- if (shouldPersistSelection && !storage) {
+ if (shouldPersistSelection && !params.storage) {
console.warn(dedent`Please set storage in getStorybookUI like this:
const StorybookUIRoot = view.getStorybookUI({
storage: {
@@ -487,7 +498,22 @@ export class View {
);
} else {
return (
-
+
+
+
+
+
+
+
+
);
}
};
diff --git a/packages/react-native/src/enhanceMetroConfig.test.ts b/packages/react-native/src/enhanceMetroConfig.test.ts
new file mode 100644
index 0000000000..fd7119e581
--- /dev/null
+++ b/packages/react-native/src/enhanceMetroConfig.test.ts
@@ -0,0 +1,93 @@
+import type { MetroConfig } from 'metro-config';
+import * as fs from 'fs';
+import * as os from 'os';
+import * as path from 'path';
+
+describe('enhanceMetroConfig', () => {
+ const config = { resolver: {}, transformer: {} } as MetroConfig;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ afterEach(() => {
+ jest.resetModules();
+ });
+
+ test('returns metro config with transformer and resolver', () => {
+ const { enhanceMetroConfig } = require('./enhanceMetroConfig');
+
+ const result = enhanceMetroConfig(config);
+
+ expect(result.transformer).toBeDefined();
+ expect(result.transformer.unstable_allowRequireContext).toBe(true);
+ expect(result.resolver).toBeDefined();
+ });
+
+ test('swaps entry point when swap data is provided', () => {
+ let tmpDir: string;
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sb-metro-test-'));
+
+ const appEntry = path.join(tmpDir, 'index.js');
+ const sbEntry = path.join(tmpDir, '.rnstorybook', 'index.tsx');
+
+ fs.writeFileSync(appEntry, '// app entry');
+ fs.mkdirSync(path.join(tmpDir, '.rnstorybook'), { recursive: true });
+ fs.writeFileSync(sbEntry, '// storybook entry');
+
+ const { enhanceMetroConfig } = require('./enhanceMetroConfig');
+
+ const result = enhanceMetroConfig(config, {
+ swap: { appEntryPoint: appEntry, storybookEntryPoint: sbEntry },
+ });
+
+ const mockResolveRequest = jest.fn(() => ({
+ filePath: appEntry,
+ type: 'sourceFile',
+ }));
+
+ const resolverResult = result.resolver.resolveRequest(
+ { resolveRequest: mockResolveRequest },
+ './index',
+ 'ios'
+ );
+
+ expect(resolverResult).toEqual({
+ filePath: sbEntry,
+ type: 'sourceFile',
+ });
+
+ fs.rmSync(tmpDir, { recursive: true, force: true });
+ });
+
+ test('does not swap when no swap data provided', () => {
+ let tmpDir: string;
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sb-metro-test-'));
+
+ const appEntry = path.join(tmpDir, 'index.js');
+ fs.writeFileSync(appEntry, '// app entry');
+
+ const { enhanceMetroConfig } = require('./enhanceMetroConfig');
+
+ const result = enhanceMetroConfig(config);
+
+ const mockResolveRequest = jest.fn(() => ({
+ filePath: appEntry,
+ type: 'sourceFile',
+ }));
+
+ const resolverResult = result.resolver.resolveRequest(
+ { resolveRequest: mockResolveRequest },
+ './index',
+ 'ios'
+ );
+
+ // No swapping — returns original resolution
+ expect(resolverResult).toEqual({
+ filePath: appEntry,
+ type: 'sourceFile',
+ });
+
+ fs.rmSync(tmpDir, { recursive: true, force: true });
+ });
+});
diff --git a/packages/react-native/src/enhanceMetroConfig.ts b/packages/react-native/src/enhanceMetroConfig.ts
new file mode 100644
index 0000000000..72f4a46c2f
--- /dev/null
+++ b/packages/react-native/src/enhanceMetroConfig.ts
@@ -0,0 +1,87 @@
+import * as path from 'path';
+import type { MetroConfig } from 'metro-config';
+import type { ResolveRequestFunction } from './metro/utils';
+
+interface EnhanceMetroOptions {
+ swap?: {
+ appEntryPoint: string;
+ storybookEntryPoint: string;
+ };
+ /**
+ * When true, removes the default Storybook UI (`@storybook/react-native-ui`)
+ * from the bundle so it can be used without its full dependency set.
+ * The `-lite` and `-common` variants remain available.
+ */
+ liteMode?: boolean;
+}
+
+export function enhanceMetroConfig(
+ config: MetroConfig,
+ options: EnhanceMetroOptions = {}
+): MetroConfig {
+ const { swap, liteMode = false } = options;
+
+ return {
+ ...config,
+ transformer: {
+ ...config.transformer,
+ unstable_allowRequireContext: true,
+ },
+ resolver: {
+ ...config.resolver,
+ resolveRequest: (context: any, moduleName: string, platform: string | null) => {
+ if (moduleName === 'tty' || moduleName === 'os') {
+ return { type: 'empty' };
+ }
+
+ const resolveFunction: ResolveRequestFunction = config?.resolver?.resolveRequest
+ ? config.resolver.resolveRequest
+ : context.resolveRequest;
+
+ const shouldUseCustomResolveConfig =
+ moduleName.startsWith('storybook') ||
+ moduleName.startsWith('@storybook') ||
+ moduleName.startsWith('uuid');
+
+ const theContext = shouldUseCustomResolveConfig
+ ? {
+ ...context,
+ unstable_enablePackageExports: true,
+ unstable_conditionNames: ['import'],
+ }
+ : context;
+
+ const resolveResult = resolveFunction(theContext, moduleName, platform);
+
+ if (resolveResult?.filePath?.includes?.('@storybook/react/template/cli')) {
+ return { type: 'empty' };
+ }
+
+ // liteMode: remove the default storybook UI from the bundle, but
+ // keep the -lite and -common variants which provide the minimal UI.
+ if (
+ liteMode &&
+ resolveResult?.filePath?.includes?.('@storybook/react-native-ui') &&
+ !resolveResult?.filePath?.includes?.('@storybook/react-native-ui-lite') &&
+ !resolveResult?.filePath?.includes?.('@storybook/react-native-ui-common')
+ ) {
+ return { type: 'empty' };
+ }
+
+ // Entry-point swapping
+ if (
+ swap &&
+ resolveResult?.filePath &&
+ path.resolve(resolveResult.filePath) === swap.appEntryPoint
+ ) {
+ return {
+ filePath: swap.storybookEntryPoint,
+ type: 'sourceFile',
+ };
+ }
+
+ return resolveResult;
+ },
+ },
+ };
+}
diff --git a/packages/react-native/src/enhanceRepackConfig.test.ts b/packages/react-native/src/enhanceRepackConfig.test.ts
new file mode 100644
index 0000000000..3b3dfb4d53
--- /dev/null
+++ b/packages/react-native/src/enhanceRepackConfig.test.ts
@@ -0,0 +1,60 @@
+describe('enhanceRepackConfig', () => {
+ test('swaps entry when swap data is provided', () => {
+ const { enhanceRepackConfig } = require('./enhanceRepackConfig');
+ const rspackConfig = { entry: './src/index.js', plugins: [] };
+
+ const result = enhanceRepackConfig(rspackConfig, {
+ swap: {
+ appEntryPoint: '/project/src/index.js',
+ storybookEntryPoint: '/project/.rnstorybook/index.tsx',
+ },
+ });
+
+ expect(result.entry).toBe('/project/.rnstorybook/index.tsx');
+ });
+
+ test('returns config unchanged when no swap data', () => {
+ const { enhanceRepackConfig } = require('./enhanceRepackConfig');
+ const rspackConfig = { entry: './src/index.js', plugins: [] };
+
+ const result = enhanceRepackConfig(rspackConfig);
+
+ expect(result).toBe(rspackConfig);
+ });
+
+ test('preserves other config properties', () => {
+ const { enhanceRepackConfig } = require('./enhanceRepackConfig');
+ const existingPlugin = { apply: jest.fn() };
+ const rspackConfig = {
+ entry: './src/index.js',
+ plugins: [existingPlugin],
+ module: { rules: [] },
+ };
+
+ const result = enhanceRepackConfig(rspackConfig, {
+ swap: {
+ appEntryPoint: '/project/src/index.js',
+ storybookEntryPoint: '/project/.rnstorybook/index.tsx',
+ },
+ });
+
+ expect(result.entry).toBe('/project/.rnstorybook/index.tsx');
+ expect(result.plugins).toHaveLength(1);
+ expect(result.plugins[0]).toBe(existingPlugin);
+ expect(result.module).toEqual({ rules: [] });
+ });
+
+ test('handles config without entry field', () => {
+ const { enhanceRepackConfig } = require('./enhanceRepackConfig');
+ const rspackConfig = { plugins: [] };
+
+ const result = enhanceRepackConfig(rspackConfig, {
+ swap: {
+ appEntryPoint: '/project/src/index.js',
+ storybookEntryPoint: '/project/.rnstorybook/index.tsx',
+ },
+ });
+
+ expect(result.entry).toBe('/project/.rnstorybook/index.tsx');
+ });
+});
diff --git a/packages/react-native/src/enhanceRepackConfig.ts b/packages/react-native/src/enhanceRepackConfig.ts
new file mode 100644
index 0000000000..00c50f993b
--- /dev/null
+++ b/packages/react-native/src/enhanceRepackConfig.ts
@@ -0,0 +1,42 @@
+interface EnhanceRepackOptions {
+ swap?: {
+ appEntryPoint: string;
+ storybookEntryPoint: string;
+ };
+ /**
+ * When true, removes the default Storybook UI (`@storybook/react-native-ui`)
+ * from the bundle so it can be used without its full dependency set.
+ * The `-lite` and `-common` variants remain available.
+ */
+ liteMode?: boolean;
+}
+
+export function enhanceRepackConfig>(
+ config: T,
+ options: EnhanceRepackOptions = {}
+): T {
+ const { swap, liteMode = false } = options;
+
+ if (!swap && !liteMode) {
+ return config;
+ }
+
+ const result: Record = { ...config };
+
+ if (swap) {
+ result.entry = swap.storybookEntryPoint;
+ }
+
+ if (liteMode) {
+ // rspack/webpack supports `false` as an alias value to produce an empty module.
+ // The `$` suffix ensures exact match so -lite and -common variants are not affected.
+ const resolve = { ...(result.resolve ?? {}) };
+ resolve.alias = {
+ ...(resolve.alias ?? {}),
+ '@storybook/react-native-ui$': false,
+ };
+ result.resolve = resolve;
+ }
+
+ return result as T;
+}
diff --git a/packages/react-native/src/env-tools.ts b/packages/react-native/src/env-tools.ts
new file mode 100644
index 0000000000..0530fc644d
--- /dev/null
+++ b/packages/react-native/src/env-tools.ts
@@ -0,0 +1,69 @@
+import type { WebsocketsOptions } from './types';
+
+export function envVariableToBoolean(
+ value: string | undefined,
+ defaultValue: any = false
+): boolean {
+ switch (value) {
+ case 'true':
+ return true;
+ case 'false':
+ return false;
+ default:
+ return !!defaultValue;
+ }
+}
+
+export function envVariableToString(
+ value: string | undefined,
+ defaultValue: string | undefined
+): string | undefined {
+ return value ?? defaultValue;
+}
+
+export function envVariableToNumber(value: string | undefined, defaultValue: number): number {
+ const parsed = parseInt(value ?? '', 10);
+ if (!isNaN(parsed)) {
+ return parsed;
+ }
+ return defaultValue;
+}
+
+export function loadWebsocketEnvOverrides(
+ websockets: WebsocketsOptions | 'auto' | undefined
+): WebsocketsOptions {
+ const envHost = envVariableToString(
+ process.env.STORYBOOK_WS_HOST,
+ websockets === 'auto' ? undefined : (websockets?.host ?? undefined)
+ );
+ const envPort = envVariableToNumber(
+ process.env.STORYBOOK_WS_PORT,
+ websockets === 'auto' ? 7007 : (websockets?.port ?? 7007)
+ );
+ const envSecured = envVariableToBoolean(process.env.STORYBOOK_WS_SECURED);
+
+ if (websockets === undefined && !envHost) {
+ return {
+ host: undefined,
+ port: undefined,
+ secured: false,
+ };
+ }
+
+ const config: WebsocketsOptions =
+ websockets === 'auto' || websockets === undefined ? {} : { ...websockets };
+
+ if (envHost) {
+ config.host = envHost;
+ }
+
+ if (envPort) {
+ config.port = envPort;
+ }
+
+ if (envSecured) {
+ config.secured = true;
+ }
+
+ return config;
+}
diff --git a/packages/react-native/src/index.ts b/packages/react-native/src/index.ts
index 70c9024b8e..6101fe1ab4 100644
--- a/packages/react-native/src/index.ts
+++ b/packages/react-native/src/index.ts
@@ -15,9 +15,26 @@ export interface Features {
ondeviceBackgrounds?: boolean;
}
+type Addon = string | { name: string; options?: Record };
+
export interface StorybookConfig {
stories: StorybookConfigBase['stories'];
- addons: Array }>;
+ /**
+ * @deprecated Use `deviceAddons` for every addon that should be bundled with
+ * the on-device preview (including `@storybook/addon-ondevice-*`, other
+ * RN-side addons, and local paths). This field will be removed in a future
+ * major version. A separate web or Node Storybook `main` file (for example
+ * for `@storybook/react-native-web-vite`) follows that package’s own API;
+ * this deprecation applies to `.rnstorybook` config typed as
+ * `StorybookConfig` from `@storybook/react-native`.
+ */
+ addons?: Addon[];
+ /**
+ * Addons loaded only at runtime on the device and merged into
+ * `storybook.requires`. Not evaluated as presets by Storybook Core, which
+ * avoids failures during server-side operations like `extract`.
+ */
+ deviceAddons?: Addon[];
// TODO move this to params
reactNative?: ReactNativeOptions;
features?: Features;
diff --git a/packages/react-native/src/metro/utils.test.ts b/packages/react-native/src/metro/utils.test.ts
new file mode 100644
index 0000000000..450827794a
--- /dev/null
+++ b/packages/react-native/src/metro/utils.test.ts
@@ -0,0 +1,79 @@
+import * as fs from 'fs';
+import * as os from 'os';
+import * as path from 'path';
+
+describe('resolveEntryPoint', () => {
+ const { resolveEntryPoint } = require('./utils');
+ let tmpDir: string;
+
+ beforeEach(() => {
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sb-entry-test-'));
+ });
+
+ afterEach(() => {
+ fs.rmSync(tmpDir, { recursive: true, force: true });
+ });
+
+ test('resolves package.json main field (Expo-style)', () => {
+ fs.writeFileSync(path.join(tmpDir, 'package.json'), JSON.stringify({ main: 'index.js' }));
+ fs.writeFileSync(path.join(tmpDir, 'index.js'), '// entry');
+
+ const result = resolveEntryPoint(tmpDir);
+ expect(result).toBe(path.join(tmpDir, 'index.js'));
+ });
+
+ test('resolves main field without extension', () => {
+ fs.writeFileSync(path.join(tmpDir, 'package.json'), JSON.stringify({ main: 'src/entry' }));
+ fs.mkdirSync(path.join(tmpDir, 'src'));
+ fs.writeFileSync(path.join(tmpDir, 'src', 'entry.tsx'), '// entry');
+
+ const result = resolveEntryPoint(tmpDir);
+ expect(result).toBe(path.join(tmpDir, 'src', 'entry.tsx'));
+ });
+
+ test('falls back to index.js when no package.json exists', () => {
+ fs.writeFileSync(path.join(tmpDir, 'index.js'), '// entry');
+
+ const result = resolveEntryPoint(tmpDir);
+ expect(result).toBe(path.join(tmpDir, 'index.js'));
+ });
+
+ test('falls back to index.ts when no package.json main', () => {
+ fs.writeFileSync(path.join(tmpDir, 'package.json'), JSON.stringify({ name: 'test-app' }));
+ fs.writeFileSync(path.join(tmpDir, 'index.ts'), '// entry');
+
+ const result = resolveEntryPoint(tmpDir);
+ expect(result).toBe(path.join(tmpDir, 'index.ts'));
+ });
+
+ test('detects expo-router entry point', () => {
+ fs.writeFileSync(
+ path.join(tmpDir, 'package.json'),
+ JSON.stringify({ main: 'expo-router/entry' })
+ );
+
+ fs.mkdirSync(path.join(tmpDir, 'node_modules', 'expo-router'), { recursive: true });
+ fs.writeFileSync(
+ path.join(tmpDir, 'node_modules', 'expo-router', 'entry.js'),
+ '// expo-router entry'
+ );
+
+ const result = resolveEntryPoint(tmpDir);
+ expect(result).toBe(path.join(tmpDir, 'node_modules', 'expo-router', 'entry.js'));
+ });
+
+ test('returns undefined when no entry file exists', () => {
+ fs.writeFileSync(path.join(tmpDir, 'package.json'), JSON.stringify({ main: 'nonexistent.js' }));
+
+ const result = resolveEntryPoint(tmpDir);
+ expect(result).toBeUndefined();
+ });
+
+ test('resolves main field with .tsx extension', () => {
+ fs.writeFileSync(path.join(tmpDir, 'package.json'), JSON.stringify({ main: 'App.tsx' }));
+ fs.writeFileSync(path.join(tmpDir, 'App.tsx'), '// entry');
+
+ const result = resolveEntryPoint(tmpDir);
+ expect(result).toBe(path.join(tmpDir, 'App.tsx'));
+ });
+});
diff --git a/packages/react-native/src/metro/utils.ts b/packages/react-native/src/metro/utils.ts
new file mode 100644
index 0000000000..23abe5247e
--- /dev/null
+++ b/packages/react-native/src/metro/utils.ts
@@ -0,0 +1,109 @@
+import * as path from 'path';
+import * as fs from 'fs';
+import type { WebsocketsOptions } from '../types';
+
+export const ENTRY_EXTENSIONS = ['js', 'jsx', 'ts', 'tsx'];
+
+export interface WithStorybookOptions {
+ configPath?: string;
+ websockets?: WebsocketsOptions | 'auto';
+ useJs?: boolean;
+ enabled?: boolean;
+ docTools?: boolean;
+ liteMode?: boolean;
+ disableUI?: boolean;
+ experimental_mcp?: boolean;
+}
+
+export type ResolveRequestFunction = (
+ context: any,
+ moduleName: string,
+ platform: string | null
+) => any;
+
+/**
+ * Resolves the application entry point for entry-point swapping.
+ *
+ * Detection order:
+ * 1. Expo Router: checks for `expo-router/entry` as the main field in package.json
+ * and resolves it from node_modules.
+ * 2. Expo / RN CLI: reads `package.json#main` and resolves it relative to the project root.
+ * 3. Fallback: defaults to `index.js` in the project root.
+ */
+export function resolveEntryPoint(projectRoot: string = process.cwd()): string | undefined {
+ const pkgJsonPath = path.resolve(projectRoot, 'package.json');
+
+ let mainField: string | undefined;
+
+ try {
+ const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
+ mainField = pkgJson.main;
+
+ // Expo Router detection: if main points to expo-router/entry, resolve from node_modules
+ if (mainField === 'expo-router/entry') {
+ const expoRouterEntry = resolveFileWithExtensions(
+ path.resolve(projectRoot, 'node_modules', 'expo-router', 'entry'),
+ ENTRY_EXTENSIONS
+ );
+
+ if (expoRouterEntry) {
+ return expoRouterEntry;
+ }
+ }
+ } catch {
+ // package.json not found or unreadable — continue with defaults
+ }
+
+ // Resolve the main field if present
+ if (mainField && mainField !== 'expo-router/entry') {
+ const resolved = resolveFileWithExtensions(
+ path.resolve(projectRoot, mainField),
+ ENTRY_EXTENSIONS
+ );
+
+ if (resolved) {
+ return resolved;
+ }
+ }
+
+ // Fallback: index.js in project root (standard RN CLI convention)
+ const fallback = resolveFileWithExtensions(path.resolve(projectRoot, 'index'), ENTRY_EXTENSIONS);
+
+ return fallback;
+}
+
+/**
+ * Resolves a file path by trying the given path as-is first, then appending each
+ * of the provided extensions. Returns the first path that exists on disk, or undefined.
+ */
+export function resolveFileWithExtensions(
+ basePath: string,
+ extensions: string[]
+): string | undefined {
+ // Try the path as-is (might already have an extension)
+ try {
+ if (fs.statSync(basePath).isFile()) {
+ return basePath;
+ }
+ } catch {
+ // Path doesn't exist or is inaccessible — try extensions
+ }
+
+ for (const ext of extensions) {
+ const candidate = `${basePath}.${ext}`;
+
+ if (fs.existsSync(candidate)) {
+ return candidate;
+ }
+ }
+
+ return undefined;
+}
+
+/**
+ * Resolves the Storybook config entry point (the file that will replace the app entry).
+ * Looks for index.(ts|tsx|js|jsx) in the config folder.
+ */
+export function resolveStorybookEntry(configPath: string): string | undefined {
+ return resolveFileWithExtensions(path.resolve(configPath, 'index'), ENTRY_EXTENSIONS);
+}
diff --git a/packages/react-native/src/metro/withStorybook.test.ts b/packages/react-native/src/metro/withStorybook.test.ts
index ebf02c3337..dd3f06b782 100644
--- a/packages/react-native/src/metro/withStorybook.test.ts
+++ b/packages/react-native/src/metro/withStorybook.test.ts
@@ -29,6 +29,9 @@ describe('withStorybook experimental_mcp', () => {
afterEach(() => {
delete process.env.STORYBOOK_DISABLE_TELEMETRY;
+ delete process.env.STORYBOOK_WS_HOST;
+ delete process.env.STORYBOOK_WS_PORT;
+ delete process.env.STORYBOOK_WS_SECURED;
});
test('starts MCP server when enabled without websockets', () => {
@@ -115,6 +118,32 @@ describe('withStorybook experimental_mcp', () => {
})
).not.toThrow();
});
+
+ test('applies STORYBOOK_WS_* env when websockets option is omitted', () => {
+ process.env.STORYBOOK_WS_HOST = '192.168.1.10';
+ process.env.STORYBOOK_WS_PORT = '8123';
+
+ withStorybook(config, {
+ configPath: '/tmp/.rnstorybook',
+ enabled: true,
+ });
+
+ expect(createChannelServer).toHaveBeenCalledWith(
+ expect.objectContaining({
+ host: '192.168.1.10',
+ port: 8123,
+ websockets: true,
+ })
+ );
+
+ expect(generate).toHaveBeenCalledWith(
+ expect.objectContaining({
+ configPath: '/tmp/.rnstorybook',
+ host: '192.168.1.10',
+ port: 8123,
+ })
+ );
+ });
});
describe('withStorybook node built-in resolution', () => {
diff --git a/packages/react-native/src/metro/withStorybook.ts b/packages/react-native/src/metro/withStorybook.ts
index ba36cb56be..20a1f777ca 100644
--- a/packages/react-native/src/metro/withStorybook.ts
+++ b/packages/react-native/src/metro/withStorybook.ts
@@ -5,6 +5,7 @@ import { optionalEnvToBoolean } from 'storybook/internal/common';
import { telemetry } from 'storybook/internal/telemetry';
import { createChannelServer } from './channelServer';
import type { WebsocketsOptions } from '../types';
+import { loadWebsocketEnvOverrides } from '../env-tools';
/**
* Options for configuring Storybook with React Native.
@@ -195,19 +196,26 @@ export function withStorybook(
};
}
- if (websockets || experimental_mcp) {
- const port = websockets === 'auto' ? 7007 : (websockets?.port ?? 7007);
- const host = websockets === 'auto' ? 'auto' : websockets?.host;
- const secured = Boolean(websockets && websockets !== 'auto' && websockets.secured);
+ if (experimental_mcp || websockets != null || process.env.STORYBOOK_WS_HOST) {
+ const resolvedWs = loadWebsocketEnvOverrides(websockets);
+ const bindHost =
+ websockets === 'auto' && !process.env.STORYBOOK_WS_HOST ? undefined : resolvedWs.host;
+ const generateHost =
+ resolvedWs.host ??
+ (websockets === 'auto' && !process.env.STORYBOOK_WS_HOST ? 'auto' : undefined);
+ const port = resolvedWs.port ?? 7007;
+ const secured = resolvedWs.secured;
+ const channelWebsocketsEnabled =
+ Boolean(websockets) || Boolean(process.env.STORYBOOK_WS_HOST) || Boolean(resolvedWs.host);
// note that in this case by passing an undefined host we only bind to the port and allow any connections i.e localhost, 127.0.0.1, 0.0.0.0, etc.
// in the generate function we try to get the ip address from the os and write it to the requires file for easier lan connection
createChannelServer({
port,
- host: host === 'auto' ? undefined : host,
+ host: bindHost,
configPath,
experimental_mcp,
- websockets: Boolean(websockets),
+ websockets: channelWebsocketsEnabled,
secured,
ssl:
websockets && websockets !== 'auto'
@@ -220,12 +228,12 @@ export function withStorybook(
: undefined,
});
- if (websockets) {
+ if (websockets != null || process.env.STORYBOOK_WS_HOST) {
generate({
configPath,
useJs,
docTools,
- host,
+ host: generateHost,
port,
secured,
});
diff --git a/packages/react-native/src/repack/withStorybook.ts b/packages/react-native/src/repack/withStorybook.ts
index 885106f8a8..cac8a9f8f5 100644
--- a/packages/react-native/src/repack/withStorybook.ts
+++ b/packages/react-native/src/repack/withStorybook.ts
@@ -2,6 +2,7 @@ import * as path from 'path';
import { generate } from '../../scripts/generate';
import { createChannelServer } from '../metro/channelServer';
import type { WebsocketsOptions } from '../types';
+import { loadWebsocketEnvOverrides } from '../env-tools';
/**
* Minimal compiler types for webpack/rspack compatibility.
@@ -169,20 +170,30 @@ export class StorybookPlugin {
experimental_mcp: boolean;
}
): void {
- const port = websockets === 'auto' ? 7007 : (websockets?.port ?? 7007);
- const host = websockets === 'auto' ? 'auto' : websockets?.host;
- const secured = Boolean(websockets && websockets !== 'auto' && websockets.secured);
+ const resolvedWs = loadWebsocketEnvOverrides(websockets);
+ const bindHost =
+ websockets === 'auto' && !process.env.STORYBOOK_WS_HOST ? undefined : resolvedWs.host;
+ const generateHost =
+ resolvedWs.host ??
+ (websockets === 'auto' && !process.env.STORYBOOK_WS_HOST ? 'auto' : undefined);
+ const port = resolvedWs.port ?? 7007;
+ const secured = resolvedWs.secured;
+ const channelWebsocketsEnabled =
+ Boolean(websockets) || Boolean(process.env.STORYBOOK_WS_HOST) || Boolean(resolvedWs.host);
// Start the channel server once (on first apply, not per-compilation)
- if ((websockets || experimental_mcp) && !this.serverStarted) {
+ if (
+ (experimental_mcp || websockets != null || process.env.STORYBOOK_WS_HOST) &&
+ !this.serverStarted
+ ) {
this.serverStarted = true;
createChannelServer({
port,
- host: host === 'auto' ? undefined : host,
+ host: bindHost,
configPath,
experimental_mcp,
- websockets: Boolean(websockets),
+ websockets: channelWebsocketsEnabled,
secured,
ssl:
websockets && websockets !== 'auto'
@@ -205,7 +216,9 @@ export class StorybookPlugin {
configPath,
useJs,
docTools,
- ...(websockets ? { host, port, secured } : {}),
+ ...(websockets != null || process.env.STORYBOOK_WS_HOST
+ ? { host: generateHost, port, secured }
+ : {}),
});
console.log('[StorybookPlugin] Generated storybook.requires');
diff --git a/packages/react-native/src/withStorybook.test.ts b/packages/react-native/src/withStorybook.test.ts
new file mode 100644
index 0000000000..27b4133057
--- /dev/null
+++ b/packages/react-native/src/withStorybook.test.ts
@@ -0,0 +1,204 @@
+import type { MetroConfig } from 'metro-config';
+
+jest.mock('./metro/channelServer', () => ({
+ createChannelServer: jest.fn(),
+}));
+
+jest.mock('../scripts/generate', () => ({
+ generate: jest.fn(),
+}));
+
+jest.mock('storybook/internal/common', () => ({
+ optionalEnvToBoolean: jest.fn(() => true),
+}));
+
+jest.mock('storybook/internal/telemetry', () => ({
+ telemetry: jest.fn(() => Promise.resolve()),
+}));
+
+describe('withStorybook (unified)', () => {
+ const metroConfig = { resolver: {}, transformer: {} } as MetroConfig;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ process.env.STORYBOOK_DISABLE_TELEMETRY = 'true';
+ });
+
+ afterEach(() => {
+ delete process.env.STORYBOOK_DISABLE_TELEMETRY;
+ delete process.env.STORYBOOK_ENABLED;
+ delete process.env.STORYBOOK_WS_HOST;
+ delete process.env.STORYBOOK_WS_PORT;
+ delete process.env.STORYBOOK_WS_SECURED;
+ jest.resetModules();
+ });
+
+ test('returns config unchanged when STORYBOOK_ENABLED is not set', () => {
+ delete process.env.STORYBOOK_ENABLED;
+
+ jest.resetModules();
+ jest.mock('./metro/channelServer', () => ({ createChannelServer: jest.fn() }));
+ jest.mock('../scripts/generate', () => ({ generate: jest.fn() }));
+ jest.mock('storybook/internal/common', () => ({ optionalEnvToBoolean: jest.fn(() => true) }));
+ jest.mock('storybook/internal/telemetry', () => ({
+ telemetry: jest.fn(() => Promise.resolve()),
+ }));
+
+ const { withStorybook } = require('./withStorybook');
+ const { generate: mockGenerate } = require('../scripts/generate');
+
+ const result = withStorybook(metroConfig);
+
+ expect(result).toBe(metroConfig);
+ expect(mockGenerate).not.toHaveBeenCalled();
+ });
+
+ test('returns config unchanged when STORYBOOK_ENABLED is false', () => {
+ process.env.STORYBOOK_ENABLED = 'false';
+
+ jest.resetModules();
+ jest.mock('./metro/channelServer', () => ({ createChannelServer: jest.fn() }));
+ jest.mock('../scripts/generate', () => ({ generate: jest.fn() }));
+ jest.mock('storybook/internal/common', () => ({ optionalEnvToBoolean: jest.fn(() => true) }));
+ jest.mock('storybook/internal/telemetry', () => ({
+ telemetry: jest.fn(() => Promise.resolve()),
+ }));
+
+ const { withStorybook } = require('./withStorybook');
+
+ const result = withStorybook(metroConfig);
+
+ expect(result).toBe(metroConfig);
+ });
+
+ test('detects Metro config and delegates when STORYBOOK_ENABLED=true', () => {
+ process.env.STORYBOOK_ENABLED = 'true';
+
+ jest.resetModules();
+ jest.mock('./metro/channelServer', () => ({ createChannelServer: jest.fn() }));
+ jest.mock('../scripts/generate', () => ({ generate: jest.fn() }));
+ jest.mock('storybook/internal/common', () => ({ optionalEnvToBoolean: jest.fn(() => true) }));
+ jest.mock('storybook/internal/telemetry', () => ({
+ telemetry: jest.fn(() => Promise.resolve()),
+ }));
+
+ const { generate: mockGenerate } = require('../scripts/generate');
+ const { withStorybook } = require('./withStorybook');
+
+ const result = withStorybook(metroConfig, {
+ configPath: '/tmp/.rnstorybook',
+ });
+
+ expect(result.transformer).toBeDefined();
+ expect(result.resolver).toBeDefined();
+ expect(mockGenerate).toHaveBeenCalled();
+ });
+
+ test('detects rspack/webpack config and calls generate', () => {
+ process.env.STORYBOOK_ENABLED = 'true';
+
+ jest.resetModules();
+ jest.mock('./metro/channelServer', () => ({ createChannelServer: jest.fn() }));
+ jest.mock('../scripts/generate', () => ({ generate: jest.fn() }));
+ jest.mock('storybook/internal/common', () => ({ optionalEnvToBoolean: jest.fn(() => true) }));
+ jest.mock('storybook/internal/telemetry', () => ({
+ telemetry: jest.fn(() => Promise.resolve()),
+ }));
+
+ const { generate: mockGenerate } = require('../scripts/generate');
+ const { withStorybook } = require('./withStorybook');
+ const rspackConfig = { plugins: [], module: { rules: [] } };
+
+ const result = withStorybook(rspackConfig, {
+ configPath: '/tmp/.rnstorybook',
+ });
+
+ expect(mockGenerate).toHaveBeenCalled();
+ // No StorybookPlugin added — config plugins preserved as-is
+ expect(result.plugins).toHaveLength(0);
+ });
+
+ test('applies ws env overrides for metro config', () => {
+ process.env.STORYBOOK_ENABLED = 'true';
+ process.env.STORYBOOK_WS_HOST = '10.0.0.5';
+ process.env.STORYBOOK_WS_PORT = '9999';
+
+ jest.resetModules();
+ jest.mock('./metro/channelServer', () => ({ createChannelServer: jest.fn() }));
+ jest.mock('../scripts/generate', () => ({ generate: jest.fn() }));
+ jest.mock('storybook/internal/common', () => ({ optionalEnvToBoolean: jest.fn(() => true) }));
+ jest.mock('storybook/internal/telemetry', () => ({
+ telemetry: jest.fn(() => Promise.resolve()),
+ }));
+
+ const { createChannelServer: mockCreateChannelServer } = require('./metro/channelServer');
+ const { withStorybook } = require('./withStorybook');
+
+ withStorybook(metroConfig, {
+ configPath: '/tmp/.rnstorybook',
+ websockets: { port: 7007 },
+ });
+
+ expect(mockCreateChannelServer).toHaveBeenCalledWith(
+ expect.objectContaining({
+ host: '10.0.0.5',
+ port: 9999,
+ })
+ );
+ });
+
+ test('applies ws env overrides for rspack config', () => {
+ process.env.STORYBOOK_ENABLED = 'true';
+ process.env.STORYBOOK_WS_HOST = '10.0.0.5';
+ process.env.STORYBOOK_WS_PORT = '9999';
+
+ jest.resetModules();
+ jest.mock('./metro/channelServer', () => ({ createChannelServer: jest.fn() }));
+ jest.mock('../scripts/generate', () => ({ generate: jest.fn() }));
+ jest.mock('storybook/internal/common', () => ({ optionalEnvToBoolean: jest.fn(() => true) }));
+ jest.mock('storybook/internal/telemetry', () => ({
+ telemetry: jest.fn(() => Promise.resolve()),
+ }));
+
+ const { createChannelServer: mockCreateChannelServer } = require('./metro/channelServer');
+ const { withStorybook } = require('./withStorybook');
+
+ const rspackConfig = { plugins: [] };
+
+ withStorybook(rspackConfig, {
+ configPath: '/tmp/.rnstorybook',
+ websockets: { port: 7007 },
+ });
+
+ expect(mockCreateChannelServer).toHaveBeenCalledWith(
+ expect.objectContaining({
+ host: '10.0.0.5',
+ port: 9999,
+ })
+ );
+ });
+
+ test('preserves existing rspack plugins', () => {
+ process.env.STORYBOOK_ENABLED = 'true';
+
+ jest.resetModules();
+ jest.mock('./metro/channelServer', () => ({ createChannelServer: jest.fn() }));
+ jest.mock('../scripts/generate', () => ({ generate: jest.fn() }));
+ jest.mock('storybook/internal/common', () => ({ optionalEnvToBoolean: jest.fn(() => true) }));
+ jest.mock('storybook/internal/telemetry', () => ({
+ telemetry: jest.fn(() => Promise.resolve()),
+ }));
+
+ const { withStorybook } = require('./withStorybook');
+ const existingPlugin = { apply: jest.fn() };
+ const rspackConfig = { plugins: [existingPlugin] };
+
+ const result = withStorybook(rspackConfig, {
+ configPath: '/tmp/.rnstorybook',
+ });
+
+ // No swap files found in test env, so config returned as-is with plugins preserved
+ expect(result.plugins).toHaveLength(1);
+ expect(result.plugins[0]).toBe(existingPlugin);
+ });
+});
diff --git a/packages/react-native/src/withStorybook.ts b/packages/react-native/src/withStorybook.ts
new file mode 100644
index 0000000000..db9a321ff6
--- /dev/null
+++ b/packages/react-native/src/withStorybook.ts
@@ -0,0 +1,93 @@
+import * as path from 'path';
+import type { MetroConfig } from 'metro-config';
+import { enhanceMetroConfig } from './enhanceMetroConfig';
+import { enhanceRepackConfig } from './enhanceRepackConfig';
+import { resolveEntryPoint, resolveStorybookEntry } from './metro/utils';
+import type { WithStorybookOptions } from './metro/utils';
+import { generate } from '../scripts/generate';
+import { createChannelServer } from './metro/channelServer';
+import { envVariableToBoolean, loadWebsocketEnvOverrides } from './env-tools';
+
+function isMetroConfig(config: unknown): config is MetroConfig {
+ return config != null && typeof config === 'object' && 'transformer' in config;
+}
+
+export function withStorybook(config: T, options: WithStorybookOptions = {}): T {
+ const enabled = envVariableToBoolean(process.env.STORYBOOK_ENABLED, options.enabled ?? false);
+ if (!enabled) {
+ return config;
+ }
+ const server = envVariableToBoolean(process.env.STORYBOOK_SERVER, true);
+ const disableUI = envVariableToBoolean(
+ process.env.STORYBOOK_DISABLE_UI,
+ options.disableUI ?? false
+ );
+ const settings = { ...options };
+
+ if (!server) {
+ settings.experimental_mcp = false;
+ }
+
+ if (disableUI) {
+ settings.docTools = false;
+ }
+
+ const defaultConfigPath = path.resolve(process.cwd(), './.rnstorybook');
+ const configPath = options.configPath || defaultConfigPath;
+ const websocketsOption = options.websockets;
+ const resolvedWs = loadWebsocketEnvOverrides(websocketsOption);
+
+ const appEntryPoint = resolveEntryPoint();
+ const storybookEntryPoint = resolveStorybookEntry(configPath);
+ const swap =
+ appEntryPoint && storybookEntryPoint ? { appEntryPoint, storybookEntryPoint } : undefined;
+
+ // Shared setup: generate + createChannelServer (used by both Metro and Repack)
+ const { useJs = false, docTools = true, experimental_mcp = false, liteMode = false } = settings;
+
+ const bindHost =
+ websocketsOption === 'auto' && !process.env.STORYBOOK_WS_HOST ? undefined : resolvedWs.host;
+ const generateHost =
+ resolvedWs.host ??
+ (websocketsOption === 'auto' && !process.env.STORYBOOK_WS_HOST ? 'auto' : undefined);
+ const port = resolvedWs.port ?? 7007;
+ const secured = resolvedWs.secured;
+ const channelWebsocketsEnabled =
+ Boolean(websocketsOption) || Boolean(process.env.STORYBOOK_WS_HOST) || Boolean(resolvedWs.host);
+
+ if (server || experimental_mcp) {
+ createChannelServer({
+ port,
+ host: bindHost,
+ configPath,
+ experimental_mcp,
+ websockets: channelWebsocketsEnabled,
+ secured,
+ ssl:
+ websocketsOption && websocketsOption !== 'auto' && secured
+ ? {
+ key: websocketsOption.key,
+ cert: websocketsOption.cert,
+ ca: websocketsOption.ca,
+ passphrase: websocketsOption.passphrase,
+ }
+ : undefined,
+ });
+ }
+
+ generate({
+ configPath,
+ useJs,
+ docTools,
+ disableUI,
+ ...(websocketsOption != null || process.env.STORYBOOK_WS_HOST
+ ? { host: generateHost, port, secured }
+ : {}),
+ });
+
+ if (isMetroConfig(config)) {
+ return enhanceMetroConfig(config, { swap, liteMode }) as unknown as T;
+ }
+
+ return enhanceRepackConfig(config as Record, { swap, liteMode }) as T;
+}
diff --git a/packages/react-native/template/cli/main.ts b/packages/react-native/template/cli/main.ts
index 335ba41f93..6e4f8c5937 100644
--- a/packages/react-native/template/cli/main.ts
+++ b/packages/react-native/template/cli/main.ts
@@ -2,7 +2,7 @@ import type { StorybookConfig } from '@storybook/react-native';
const main: StorybookConfig = {
stories: ['./stories/**/*.stories.?(ts|tsx|js|jsx)'],
- addons: ['@storybook/addon-ondevice-controls', '@storybook/addon-ondevice-actions'],
+ deviceAddons: ['@storybook/addon-ondevice-controls', '@storybook/addon-ondevice-actions'],
};
export default main;
diff --git a/packages/react-native/tsup.config.ts b/packages/react-native/tsup.config.ts
index cb317c62be..f8d2458919 100644
--- a/packages/react-native/tsup.config.ts
+++ b/packages/react-native/tsup.config.ts
@@ -5,6 +5,7 @@ export default defineConfig((options) => {
entry: [
'src/index.ts',
'src/preview.ts',
+ 'src/withStorybook.ts',
'src/metro/withStorybook.ts',
'src/repack/withStorybook.ts',
'src/stub.tsx',
@@ -17,6 +18,7 @@ export default defineConfig((options) => {
entry: [
'src/index.ts',
'src/preview.ts',
+ 'src/withStorybook.ts',
'src/metro/withStorybook.ts',
'src/repack/withStorybook.ts',
'src/node.ts',
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 9ba8ce521f..2b82c0e062 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -230,6 +230,139 @@ importers:
specifier: ^8.0.5
version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@1.21.7)(terser@5.46.1)(yaml@2.8.3)
+ examples/expo-new-wrapper-example:
+ dependencies:
+ '@expo/metro-runtime':
+ specifier: ~55.0.7
+ version: 55.0.9(@expo/dom-webview@55.0.5)(expo@55.0.14)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ '@gorhom/bottom-sheet':
+ specifier: ^5.2.8
+ version: 5.2.9(@types/react@19.2.14)(react-native-gesture-handler@2.30.1(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-reanimated@4.2.3(react-native-worklets@0.7.2(@babel/core@7.29.0)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ '@react-native-async-storage/async-storage':
+ specifier: 2.2.0
+ version: 2.2.0(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))
+ '@react-native-community/datetimepicker':
+ specifier: 8.6.0
+ version: 8.6.0(expo@55.0.14)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ '@react-native-community/slider':
+ specifier: 5.1.2
+ version: 5.1.2
+ '@storybook/addon-ondevice-actions':
+ specifier: ^10.3.2
+ version: link:../../packages/ondevice-actions
+ '@storybook/addon-ondevice-backgrounds':
+ specifier: ^10.3.2
+ version: link:../../packages/ondevice-backgrounds
+ '@storybook/addon-ondevice-controls':
+ specifier: ^10.3.2
+ version: link:../../packages/ondevice-controls
+ '@storybook/addon-ondevice-notes':
+ specifier: ^10.3.2
+ version: link:../../packages/ondevice-notes
+ '@storybook/addon-react-native-server':
+ specifier: ^1.0.1
+ version: 1.0.1
+ '@storybook/react':
+ specifier: ^10.3.2
+ version: 10.3.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.3.2(@testing-library/dom@10.4.1)(prettier@3.8.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(typescript@5.9.3)
+ '@storybook/react-native':
+ specifier: ^10.3.2
+ version: link:../../packages/react-native
+ '@storybook/react-native-ui-lite':
+ specifier: ^10.3.2
+ version: link:../../packages/react-native-ui-lite
+ '@storybook/react-native-web-vite':
+ specifier: ^10.3.2
+ version: 10.3.2(esbuild@0.27.7)(react-dom@19.2.0(react@19.2.0))(react-native-web@0.21.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(rollup@4.60.1)(storybook@10.3.2(@testing-library/dom@10.4.1)(prettier@3.8.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@1.21.7)(terser@5.46.1)(yaml@2.8.3))(webpack@5.106.1(esbuild@0.27.7))
+ babel-plugin-react-compiler:
+ specifier: ^1.0.0
+ version: 1.0.0
+ expo:
+ specifier: ^55.0.14
+ version: 55.0.14(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.2(@babel/core@7.29.0)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ expo-updates:
+ specifier: ~55.0.16
+ version: 55.0.20(expo@55.0.14)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ react:
+ specifier: 19.2.0
+ version: 19.2.0
+ react-compiler-runtime:
+ specifier: ^1.0.0
+ version: 1.0.0(react@19.2.0)
+ react-dom:
+ specifier: 19.2.0
+ version: 19.2.0(react@19.2.0)
+ react-native:
+ specifier: 0.83.4
+ version: 0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0)
+ react-native-gesture-handler:
+ specifier: ~2.30.0
+ version: 2.30.1(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ react-native-reanimated:
+ specifier: ~4.2.1
+ version: 4.2.3(react-native-worklets@0.7.2(@babel/core@7.29.0)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ react-native-safe-area-context:
+ specifier: ^5
+ version: 5.7.0(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ react-native-svg:
+ specifier: 15.15.3
+ version: 15.15.3(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ react-native-web:
+ specifier: ^0.21.2
+ version: 0.21.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ react-native-worklets:
+ specifier: 0.7.2
+ version: 0.7.2(@babel/core@7.29.0)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ storybook:
+ specifier: ^10.3.2
+ version: 10.3.2(@testing-library/dom@10.4.1)(prettier@3.8.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ storybook-addon-deep-controls:
+ specifier: ^0.10.0
+ version: 0.10.0(storybook@10.3.2(@testing-library/dom@10.4.1)(prettier@3.8.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))
+ ws:
+ specifier: ^8.20.0
+ version: 8.20.0
+ devDependencies:
+ '@babel/core':
+ specifier: ^7.26.0
+ version: 7.29.0
+ '@dannyhw/rozenite-storybook':
+ specifier: 0.0.2
+ version: 0.0.2(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ '@rozenite/metro':
+ specifier: ^1.6.0
+ version: 1.7.0
+ '@testing-library/react-native':
+ specifier: 14.0.0-beta.0
+ version: 14.0.0-beta.0(jest@29.7.0(@types/node@25.6.0)(babel-plugin-macros@3.1.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(test-renderer@0.15.0(@types/react@19.2.14)(react@19.2.0))
+ '@types/react':
+ specifier: ~19.2.14
+ version: 19.2.14
+ '@types/ws':
+ specifier: ^8.18.1
+ version: 8.18.1
+ babel-plugin-react-docgen-typescript:
+ specifier: ^1.5.1
+ version: 1.5.1(@babel/core@7.29.0)(typescript@5.9.3)
+ expo-atlas:
+ specifier: ^0.4.3
+ version: 0.4.3(expo@55.0.14)
+ jest:
+ specifier: ^29.7.0
+ version: 29.7.0(@types/node@25.6.0)(babel-plugin-macros@3.1.0)
+ jest-expo:
+ specifier: ~55.0.11
+ version: 55.0.15(@babel/core@7.29.0)(expo@55.0.14)(jest@29.7.0(@types/node@25.6.0)(babel-plugin-macros@3.1.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ test-renderer:
+ specifier: ^0.15.0
+ version: 0.15.0(@types/react@19.2.14)(react@19.2.0)
+ typescript:
+ specifier: ~5.9.3
+ version: 5.9.3
+ vite:
+ specifier: ^8.0.5
+ version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@1.21.7)(terser@5.46.1)(yaml@2.8.3)
+
packages/ondevice-actions:
dependencies:
'@storybook/react-native-theming':
diff --git a/tests/scripts/generate.test.ts b/tests/scripts/generate.test.ts
index 684c461116..5a46a33df6 100644
--- a/tests/scripts/generate.test.ts
+++ b/tests/scripts/generate.test.ts
@@ -274,5 +274,71 @@ describe('loader', () => {
t.assert.snapshot(fileContentMock);
});
});
+
+ describe('when addons are in deviceAddons', () => {
+ it('writes the addon imports from deviceAddons', async (t) => {
+ mock.method(require('fs'), 'writeFileSync', mockFs.writeFileSync);
+ await generate({ configPath: 'scripts/mocks/device-addons' });
+ mock.reset();
+
+ assert.strictEqual(
+ pathMock,
+ path.resolve(__dirname, 'mocks/device-addons/storybook.requires.ts')
+ );
+ t.assert.snapshot(fileContentMock);
+ });
+ });
+
+ describe('when addons are split between addons and deviceAddons', () => {
+ it('writes imports from both addons and deviceAddons', async (t) => {
+ mock.method(require('fs'), 'writeFileSync', mockFs.writeFileSync);
+ await generate({ configPath: 'scripts/mocks/mixed-addons' });
+ mock.reset();
+
+ assert.strictEqual(
+ pathMock,
+ path.resolve(__dirname, 'mocks/mixed-addons/storybook.requires.ts')
+ );
+ t.assert.snapshot(fileContentMock);
+ });
+
+ it('logs a deprecation warning when addons is non-empty', async () => {
+ const warn = mock.method(console, 'warn', mock.fn());
+ mock.method(require('fs'), 'writeFileSync', mockFs.writeFileSync);
+ await generate({ configPath: 'scripts/mocks/mixed-addons' });
+ mock.reset();
+
+ assert.strictEqual(warn.mock.callCount(), 1);
+ const msg = String(warn.mock.calls[0].arguments[0]);
+ assert.ok(msg.includes('deprecated'));
+ assert.ok(msg.includes('deviceAddons'));
+ assert.ok(msg.includes('deprecating-addons-in-rnstorybook-main'));
+ });
+ });
+
+ describe('legacy on-device addons under main.addons', () => {
+ it('logs a deprecation warning', async () => {
+ const warn = mock.method(console, 'warn', mock.fn());
+ mock.method(require('fs'), 'writeFileSync', mockFs.writeFileSync);
+ await generate({ configPath: 'scripts/mocks/legacy-ondevice-in-addons' });
+ mock.reset();
+
+ assert.strictEqual(warn.mock.callCount(), 1);
+ const msg = String(warn.mock.calls[0].arguments[0]);
+ assert.ok(msg.includes('deprecated'));
+ assert.ok(msg.includes('deviceAddons'));
+ assert.ok(msg.includes('@storybook/addon-ondevice-controls'));
+ assert.ok(msg.includes('deprecating-addons-in-rnstorybook-main'));
+ });
+
+ it('does not warn when on-device addons are only in deviceAddons', async () => {
+ const warn = mock.method(console, 'warn', mock.fn());
+ mock.method(require('fs'), 'writeFileSync', mockFs.writeFileSync);
+ await generate({ configPath: 'scripts/mocks/device-addons' });
+ mock.reset();
+
+ assert.strictEqual(warn.mock.callCount(), 0);
+ });
+ });
});
});
diff --git a/tests/scripts/generate.test.ts.snapshot b/tests/scripts/generate.test.ts.snapshot
index e1ea2632b4..86464be8d5 100644
--- a/tests/scripts/generate.test.ts.snapshot
+++ b/tests/scripts/generate.test.ts.snapshot
@@ -1,51 +1,59 @@
+exports[`loader > writeRequires > when addons are in deviceAddons > writes the addon imports from deviceAddons 1`] = `
+"/* do not change this file, it is auto generated by storybook. */\\n/// \\nimport { start, updateView, View, type Features } from '@storybook/react-native';\\n\\n\\nimport \\"@storybook/addon-ondevice-notes/register\\";\\nimport \\"@storybook/addon-ondevice-controls/register\\";\\nimport \\"@storybook/addon-ondevice-backgrounds/register\\";\\nimport \\"@storybook/addon-ondevice-actions/register\\";\\n\\nconst normalizedStories = [\\n {\\n titlePrefix: \\"\\",\\n directory: \\"./scripts/mocks/device-addons\\",\\n files: \\"FakeStory.stories.tsx\\",\\n importPathMatcher: /^\\\\.[\\\\\\\\/](?:FakeStory\\\\.stories\\\\.tsx)$/,\\n req: require.context(\\n './',\\n false,\\n /^\\\\.[\\\\\\\\/](?:FakeStory\\\\.stories\\\\.tsx)$/\\n ),\\n }\\n];\\n\\n\\ndeclare global {\\n var view: View;\\n var STORIES: typeof normalizedStories;\\n var STORYBOOK_WEBSOCKET:\\n | { host?: string; port?: number; secured?: boolean }\\n | undefined;\\n var FEATURES: Features;\\n}\\n\\n\\nconst annotations = [\\n require('./preview'),\\n require(\\"@storybook/react-native/preview\\")\\n];\\n\\nglobalThis.STORIES = normalizedStories;\\n\\n\\nmodule?.hot?.accept?.();\\n\\nconst options = {}\\n\\nif (!globalThis.view) {\\n globalThis.view = start({\\n annotations,\\n storyEntries: normalizedStories,\\n options,\\n });\\n} else {\\n updateView(globalThis.view, annotations, normalizedStories, options);\\n}\\n\\nexport const view: View = globalThis.view;\\n"
+`;
+
+exports[`loader > writeRequires > when addons are split between addons and deviceAddons > writes imports from both addons and deviceAddons 1`] = `
+"/* do not change this file, it is auto generated by storybook. */\\n/// \\nimport { start, updateView, View, type Features } from '@storybook/react-native';\\n\\n\\nimport \\"@storybook/addon-ondevice-notes/register\\";\\nimport \\"@storybook/addon-ondevice-controls/register\\";\\nimport \\"@storybook/addon-ondevice-backgrounds/register\\";\\nimport \\"@storybook/addon-ondevice-actions/register\\";\\n\\nconst normalizedStories = [\\n {\\n titlePrefix: \\"\\",\\n directory: \\"./scripts/mocks/mixed-addons\\",\\n files: \\"FakeStory.stories.tsx\\",\\n importPathMatcher: /^\\\\.[\\\\\\\\/](?:FakeStory\\\\.stories\\\\.tsx)$/,\\n req: require.context(\\n './',\\n false,\\n /^\\\\.[\\\\\\\\/](?:FakeStory\\\\.stories\\\\.tsx)$/\\n ),\\n }\\n];\\n\\n\\ndeclare global {\\n var view: View;\\n var STORIES: typeof normalizedStories;\\n var STORYBOOK_WEBSOCKET:\\n | { host?: string; port?: number; secured?: boolean }\\n | undefined;\\n var FEATURES: Features;\\n}\\n\\n\\nconst annotations = [\\n require('./preview'),\\n require(\\"@storybook/react-native/preview\\")\\n];\\n\\nglobalThis.STORIES = normalizedStories;\\n\\n\\nmodule?.hot?.accept?.();\\n\\nconst options = {}\\n\\nif (!globalThis.view) {\\n globalThis.view = start({\\n annotations,\\n storyEntries: normalizedStories,\\n options,\\n });\\n} else {\\n updateView(globalThis.view, annotations, normalizedStories, options);\\n}\\n\\nexport const view: View = globalThis.view;\\n"
+`;
+
exports[`loader > writeRequires > when features are provided > sets feature flags on globalThis.FEATURES 1`] = `
-"/* do not change this file, it is auto generated by storybook. */\\n/// \\nimport { start, updateView, View, type Features } from '@storybook/react-native';\\n\\nimport \\"@storybook/addon-ondevice-notes/register\\";\\nimport \\"@storybook/addon-ondevice-controls/register\\";\\nimport \\"@storybook/addon-ondevice-actions/register\\";\\n\\nconst normalizedStories = [\\n {\\n titlePrefix: \\"\\",\\n directory: \\"./scripts/mocks/with-features\\",\\n files: \\"FakeStory.stories.tsx\\",\\n importPathMatcher: /^\\\\.[\\\\\\\\/](?:FakeStory\\\\.stories\\\\.tsx)$/,\\n req: require.context(\\n './',\\n false,\\n /^\\\\.[\\\\\\\\/](?:FakeStory\\\\.stories\\\\.tsx)$/\\n ),\\n }\\n];\\n\\n\\ndeclare global {\\n var view: View;\\n var STORIES: typeof normalizedStories;\\n var STORYBOOK_WEBSOCKET:\\n | { host?: string; port?: number; secured?: boolean }\\n | undefined;\\n var FEATURES: Features;\\n}\\n\\n\\nconst annotations = [\\n require('./preview'),\\n require(\\"@storybook/react-native/preview\\")\\n];\\n\\nglobalThis.STORIES = normalizedStories;\\n\\n\\nmodule?.hot?.accept?.();\\n\\nglobalThis.FEATURES.ondeviceBackgrounds = true;\\n\\n\\n\\nif (!globalThis.view) {\\n globalThis.view = start({\\n annotations,\\n storyEntries: normalizedStories,\\n\\n });\\n} else {\\n updateView(globalThis.view, annotations, normalizedStories);\\n}\\n\\nexport const view: View = globalThis.view;\\n"
+"/* do not change this file, it is auto generated by storybook. */\\n/// \\nimport { start, updateView, View, type Features } from '@storybook/react-native';\\n\\n\\nimport \\"@storybook/addon-ondevice-notes/register\\";\\nimport \\"@storybook/addon-ondevice-controls/register\\";\\nimport \\"@storybook/addon-ondevice-actions/register\\";\\n\\nconst normalizedStories = [\\n {\\n titlePrefix: \\"\\",\\n directory: \\"./scripts/mocks/with-features\\",\\n files: \\"FakeStory.stories.tsx\\",\\n importPathMatcher: /^\\\\.[\\\\\\\\/](?:FakeStory\\\\.stories\\\\.tsx)$/,\\n req: require.context(\\n './',\\n false,\\n /^\\\\.[\\\\\\\\/](?:FakeStory\\\\.stories\\\\.tsx)$/\\n ),\\n }\\n];\\n\\n\\ndeclare global {\\n var view: View;\\n var STORIES: typeof normalizedStories;\\n var STORYBOOK_WEBSOCKET:\\n | { host?: string; port?: number; secured?: boolean }\\n | undefined;\\n var FEATURES: Features;\\n}\\n\\n\\nconst annotations = [\\n require('./preview'),\\n require(\\"@storybook/react-native/preview\\")\\n];\\n\\nglobalThis.STORIES = normalizedStories;\\n\\n\\nmodule?.hot?.accept?.();\\n\\nglobalThis.FEATURES.ondeviceBackgrounds = true;\\n\\nconst options = {}\\n\\nif (!globalThis.view) {\\n globalThis.view = start({\\n annotations,\\n storyEntries: normalizedStories,\\n options,\\n });\\n} else {\\n updateView(globalThis.view, annotations, normalizedStories, options);\\n}\\n\\nexport const view: View = globalThis.view;\\n"
`;
exports[`loader > writeRequires > when host and port are provided > includes STORYBOOK_WEBSOCKET with host and port 1`] = `
-"/* do not change this file, it is auto generated by storybook. */\\n/// \\nimport { start, updateView, View, type Features } from '@storybook/react-native';\\n\\nimport \\"@storybook/addon-ondevice-notes/register\\";\\nimport \\"@storybook/addon-ondevice-controls/register\\";\\nimport \\"@storybook/addon-ondevice-backgrounds/register\\";\\nimport \\"@storybook/addon-ondevice-actions/register\\";\\n\\nconst normalizedStories = [\\n {\\n titlePrefix: \\"\\",\\n directory: \\"./scripts/mocks/all-config-files\\",\\n files: \\"FakeStory.stories.tsx\\",\\n importPathMatcher: /^\\\\.[\\\\\\\\/](?:FakeStory\\\\.stories\\\\.tsx)$/,\\n req: require.context(\\n './',\\n false,\\n /^\\\\.[\\\\\\\\/](?:FakeStory\\\\.stories\\\\.tsx)$/\\n ),\\n }\\n];\\n\\n\\ndeclare global {\\n var view: View;\\n var STORIES: typeof normalizedStories;\\n var STORYBOOK_WEBSOCKET:\\n | { host?: string; port?: number; secured?: boolean }\\n | undefined;\\n var FEATURES: Features;\\n}\\n\\n\\nconst annotations = [\\n require('./preview'),\\n require(\\"@storybook/react-native/preview\\")\\n];\\n\\nglobalThis.STORIES = normalizedStories;\\nglobalThis.STORYBOOK_WEBSOCKET = {\\n host: '192.168.1.100',\\n port: 8080,\\n secured: false,\\n};\\n\\nmodule?.hot?.accept?.();\\n\\n\\n\\nif (!globalThis.view) {\\n globalThis.view = start({\\n annotations,\\n storyEntries: normalizedStories,\\n\\n });\\n} else {\\n updateView(globalThis.view, annotations, normalizedStories);\\n}\\n\\nexport const view: View = globalThis.view;\\n"
+"/* do not change this file, it is auto generated by storybook. */\\n/// \\nimport { start, updateView, View, type Features } from '@storybook/react-native';\\n\\n\\nimport \\"@storybook/addon-ondevice-notes/register\\";\\nimport \\"@storybook/addon-ondevice-controls/register\\";\\nimport \\"@storybook/addon-ondevice-backgrounds/register\\";\\nimport \\"@storybook/addon-ondevice-actions/register\\";\\n\\nconst normalizedStories = [\\n {\\n titlePrefix: \\"\\",\\n directory: \\"./scripts/mocks/all-config-files\\",\\n files: \\"FakeStory.stories.tsx\\",\\n importPathMatcher: /^\\\\.[\\\\\\\\/](?:FakeStory\\\\.stories\\\\.tsx)$/,\\n req: require.context(\\n './',\\n false,\\n /^\\\\.[\\\\\\\\/](?:FakeStory\\\\.stories\\\\.tsx)$/\\n ),\\n }\\n];\\n\\n\\ndeclare global {\\n var view: View;\\n var STORIES: typeof normalizedStories;\\n var STORYBOOK_WEBSOCKET:\\n | { host?: string; port?: number; secured?: boolean }\\n | undefined;\\n var FEATURES: Features;\\n}\\n\\n\\nconst annotations = [\\n require('./preview'),\\n require(\\"@storybook/react-native/preview\\")\\n];\\n\\nglobalThis.STORIES = normalizedStories;\\nglobalThis.STORYBOOK_WEBSOCKET = {\\n host: '192.168.1.100',\\n port: 8080,\\n secured: false,\\n};\\n\\nmodule?.hot?.accept?.();\\n\\nconst options = {}\\n\\nif (!globalThis.view) {\\n globalThis.view = start({\\n annotations,\\n storyEntries: normalizedStories,\\n options,\\n });\\n} else {\\n updateView(globalThis.view, annotations, normalizedStories, options);\\n}\\n\\nexport const view: View = globalThis.view;\\n"
`;
exports[`loader > writeRequires > when host and port are provided with useJs > includes STORYBOOK_WEBSOCKET in JS file 1`] = `
-"/* do not change this file, it is auto generated by storybook. */\\nimport { start, updateView } from '@storybook/react-native';\\n\\nimport \\"@storybook/addon-ondevice-notes/register\\";\\nimport \\"@storybook/addon-ondevice-controls/register\\";\\nimport \\"@storybook/addon-ondevice-backgrounds/register\\";\\nimport \\"@storybook/addon-ondevice-actions/register\\";\\n\\nconst normalizedStories = [\\n {\\n titlePrefix: \\"\\",\\n directory: \\"./scripts/mocks/all-config-files\\",\\n files: \\"FakeStory.stories.tsx\\",\\n importPathMatcher: /^\\\\.[\\\\\\\\/](?:FakeStory\\\\.stories\\\\.tsx)$/,\\n req: require.context(\\n './',\\n false,\\n /^\\\\.[\\\\\\\\/](?:FakeStory\\\\.stories\\\\.tsx)$/\\n ),\\n }\\n];\\n\\n\\n\\nconst annotations = [\\n require('./preview'),\\n require(\\"@storybook/react-native/preview\\")\\n];\\n\\nglobalThis.STORIES = normalizedStories;\\nglobalThis.STORYBOOK_WEBSOCKET = {\\n host: '192.168.1.100',\\n port: 8080,\\n secured: false,\\n};\\n\\nmodule?.hot?.accept?.();\\n\\n\\n\\nif (!globalThis.view) {\\n globalThis.view = start({\\n annotations,\\n storyEntries: normalizedStories,\\n\\n });\\n} else {\\n updateView(globalThis.view, annotations, normalizedStories);\\n}\\n\\nexport const view = globalThis.view;\\n"
+"/* do not change this file, it is auto generated by storybook. */\\nimport { start, updateView } from '@storybook/react-native';\\n\\n\\nimport \\"@storybook/addon-ondevice-notes/register\\";\\nimport \\"@storybook/addon-ondevice-controls/register\\";\\nimport \\"@storybook/addon-ondevice-backgrounds/register\\";\\nimport \\"@storybook/addon-ondevice-actions/register\\";\\n\\nconst normalizedStories = [\\n {\\n titlePrefix: \\"\\",\\n directory: \\"./scripts/mocks/all-config-files\\",\\n files: \\"FakeStory.stories.tsx\\",\\n importPathMatcher: /^\\\\.[\\\\\\\\/](?:FakeStory\\\\.stories\\\\.tsx)$/,\\n req: require.context(\\n './',\\n false,\\n /^\\\\.[\\\\\\\\/](?:FakeStory\\\\.stories\\\\.tsx)$/\\n ),\\n }\\n];\\n\\n\\n\\nconst annotations = [\\n require('./preview'),\\n require(\\"@storybook/react-native/preview\\")\\n];\\n\\nglobalThis.STORIES = normalizedStories;\\nglobalThis.STORYBOOK_WEBSOCKET = {\\n host: '192.168.1.100',\\n port: 8080,\\n secured: false,\\n};\\n\\nmodule?.hot?.accept?.();\\n\\nconst options = {}\\n\\nif (!globalThis.view) {\\n globalThis.view = start({\\n annotations,\\n storyEntries: normalizedStories,\\n options,\\n });\\n} else {\\n updateView(globalThis.view, annotations, normalizedStories, options);\\n}\\n\\nexport const view = globalThis.view;\\n"
`;
exports[`loader > writeRequires > when host is not provided > does not include STORYBOOK_WEBSOCKET assignment 1`] = `
-"/* do not change this file, it is auto generated by storybook. */\\n/// \\nimport { start, updateView, View, type Features } from '@storybook/react-native';\\n\\nimport \\"@storybook/addon-ondevice-notes/register\\";\\nimport \\"@storybook/addon-ondevice-controls/register\\";\\nimport \\"@storybook/addon-ondevice-backgrounds/register\\";\\nimport \\"@storybook/addon-ondevice-actions/register\\";\\n\\nconst normalizedStories = [\\n {\\n titlePrefix: \\"\\",\\n directory: \\"./scripts/mocks/all-config-files\\",\\n files: \\"FakeStory.stories.tsx\\",\\n importPathMatcher: /^\\\\.[\\\\\\\\/](?:FakeStory\\\\.stories\\\\.tsx)$/,\\n req: require.context(\\n './',\\n false,\\n /^\\\\.[\\\\\\\\/](?:FakeStory\\\\.stories\\\\.tsx)$/\\n ),\\n }\\n];\\n\\n\\ndeclare global {\\n var view: View;\\n var STORIES: typeof normalizedStories;\\n var STORYBOOK_WEBSOCKET:\\n | { host?: string; port?: number; secured?: boolean }\\n | undefined;\\n var FEATURES: Features;\\n}\\n\\n\\nconst annotations = [\\n require('./preview'),\\n require(\\"@storybook/react-native/preview\\")\\n];\\n\\nglobalThis.STORIES = normalizedStories;\\n\\n\\nmodule?.hot?.accept?.();\\n\\n\\n\\nif (!globalThis.view) {\\n globalThis.view = start({\\n annotations,\\n storyEntries: normalizedStories,\\n\\n });\\n} else {\\n updateView(globalThis.view, annotations, normalizedStories);\\n}\\n\\nexport const view: View = globalThis.view;\\n"
+"/* do not change this file, it is auto generated by storybook. */\\n/// \\nimport { start, updateView, View, type Features } from '@storybook/react-native';\\n\\n\\nimport \\"@storybook/addon-ondevice-notes/register\\";\\nimport \\"@storybook/addon-ondevice-controls/register\\";\\nimport \\"@storybook/addon-ondevice-backgrounds/register\\";\\nimport \\"@storybook/addon-ondevice-actions/register\\";\\n\\nconst normalizedStories = [\\n {\\n titlePrefix: \\"\\",\\n directory: \\"./scripts/mocks/all-config-files\\",\\n files: \\"FakeStory.stories.tsx\\",\\n importPathMatcher: /^\\\\.[\\\\\\\\/](?:FakeStory\\\\.stories\\\\.tsx)$/,\\n req: require.context(\\n './',\\n false,\\n /^\\\\.[\\\\\\\\/](?:FakeStory\\\\.stories\\\\.tsx)$/\\n ),\\n }\\n];\\n\\n\\ndeclare global {\\n var view: View;\\n var STORIES: typeof normalizedStories;\\n var STORYBOOK_WEBSOCKET:\\n | { host?: string; port?: number; secured?: boolean }\\n | undefined;\\n var FEATURES: Features;\\n}\\n\\n\\nconst annotations = [\\n require('./preview'),\\n require(\\"@storybook/react-native/preview\\")\\n];\\n\\nglobalThis.STORIES = normalizedStories;\\n\\n\\nmodule?.hot?.accept?.();\\n\\nconst options = {}\\n\\nif (!globalThis.view) {\\n globalThis.view = start({\\n annotations,\\n storyEntries: normalizedStories,\\n options,\\n });\\n} else {\\n updateView(globalThis.view, annotations, normalizedStories, options);\\n}\\n\\nexport const view: View = globalThis.view;\\n"
`;
exports[`loader > writeRequires > when no features are provided > does not include FEATURES assignments 1`] = `
-"/* do not change this file, it is auto generated by storybook. */\\n/// \\nimport { start, updateView, View, type Features } from '@storybook/react-native';\\n\\nimport \\"@storybook/addon-ondevice-notes/register\\";\\nimport \\"@storybook/addon-ondevice-controls/register\\";\\nimport \\"@storybook/addon-ondevice-backgrounds/register\\";\\nimport \\"@storybook/addon-ondevice-actions/register\\";\\n\\nconst normalizedStories = [\\n {\\n titlePrefix: \\"\\",\\n directory: \\"./scripts/mocks/all-config-files\\",\\n files: \\"FakeStory.stories.tsx\\",\\n importPathMatcher: /^\\\\.[\\\\\\\\/](?:FakeStory\\\\.stories\\\\.tsx)$/,\\n req: require.context(\\n './',\\n false,\\n /^\\\\.[\\\\\\\\/](?:FakeStory\\\\.stories\\\\.tsx)$/\\n ),\\n }\\n];\\n\\n\\ndeclare global {\\n var view: View;\\n var STORIES: typeof normalizedStories;\\n var STORYBOOK_WEBSOCKET:\\n | { host?: string; port?: number; secured?: boolean }\\n | undefined;\\n var FEATURES: Features;\\n}\\n\\n\\nconst annotations = [\\n require('./preview'),\\n require(\\"@storybook/react-native/preview\\")\\n];\\n\\nglobalThis.STORIES = normalizedStories;\\n\\n\\nmodule?.hot?.accept?.();\\n\\n\\n\\nif (!globalThis.view) {\\n globalThis.view = start({\\n annotations,\\n storyEntries: normalizedStories,\\n\\n });\\n} else {\\n updateView(globalThis.view, annotations, normalizedStories);\\n}\\n\\nexport const view: View = globalThis.view;\\n"
+"/* do not change this file, it is auto generated by storybook. */\\n/// \\nimport { start, updateView, View, type Features } from '@storybook/react-native';\\n\\n\\nimport \\"@storybook/addon-ondevice-notes/register\\";\\nimport \\"@storybook/addon-ondevice-controls/register\\";\\nimport \\"@storybook/addon-ondevice-backgrounds/register\\";\\nimport \\"@storybook/addon-ondevice-actions/register\\";\\n\\nconst normalizedStories = [\\n {\\n titlePrefix: \\"\\",\\n directory: \\"./scripts/mocks/all-config-files\\",\\n files: \\"FakeStory.stories.tsx\\",\\n importPathMatcher: /^\\\\.[\\\\\\\\/](?:FakeStory\\\\.stories\\\\.tsx)$/,\\n req: require.context(\\n './',\\n false,\\n /^\\\\.[\\\\\\\\/](?:FakeStory\\\\.stories\\\\.tsx)$/\\n ),\\n }\\n];\\n\\n\\ndeclare global {\\n var view: View;\\n var STORIES: typeof normalizedStories;\\n var STORYBOOK_WEBSOCKET:\\n | { host?: string; port?: number; secured?: boolean }\\n | undefined;\\n var FEATURES: Features;\\n}\\n\\n\\nconst annotations = [\\n require('./preview'),\\n require(\\"@storybook/react-native/preview\\")\\n];\\n\\nglobalThis.STORIES = normalizedStories;\\n\\n\\nmodule?.hot?.accept?.();\\n\\nconst options = {}\\n\\nif (!globalThis.view) {\\n globalThis.view = start({\\n annotations,\\n storyEntries: normalizedStories,\\n options,\\n });\\n} else {\\n updateView(globalThis.view, annotations, normalizedStories, options);\\n}\\n\\nexport const view: View = globalThis.view;\\n"
`;
exports[`loader > writeRequires > when only host is provided > includes STORYBOOK_WEBSOCKET with host and default port 1`] = `
-"/* do not change this file, it is auto generated by storybook. */\\n/// \\nimport { start, updateView, View, type Features } from '@storybook/react-native';\\n\\nimport \\"@storybook/addon-ondevice-notes/register\\";\\nimport \\"@storybook/addon-ondevice-controls/register\\";\\nimport \\"@storybook/addon-ondevice-backgrounds/register\\";\\nimport \\"@storybook/addon-ondevice-actions/register\\";\\n\\nconst normalizedStories = [\\n {\\n titlePrefix: \\"\\",\\n directory: \\"./scripts/mocks/all-config-files\\",\\n files: \\"FakeStory.stories.tsx\\",\\n importPathMatcher: /^\\\\.[\\\\\\\\/](?:FakeStory\\\\.stories\\\\.tsx)$/,\\n req: require.context(\\n './',\\n false,\\n /^\\\\.[\\\\\\\\/](?:FakeStory\\\\.stories\\\\.tsx)$/\\n ),\\n }\\n];\\n\\n\\ndeclare global {\\n var view: View;\\n var STORIES: typeof normalizedStories;\\n var STORYBOOK_WEBSOCKET:\\n | { host?: string; port?: number; secured?: boolean }\\n | undefined;\\n var FEATURES: Features;\\n}\\n\\n\\nconst annotations = [\\n require('./preview'),\\n require(\\"@storybook/react-native/preview\\")\\n];\\n\\nglobalThis.STORIES = normalizedStories;\\nglobalThis.STORYBOOK_WEBSOCKET = {\\n host: 'localhost',\\n port: 7007,\\n secured: false,\\n};\\n\\nmodule?.hot?.accept?.();\\n\\n\\n\\nif (!globalThis.view) {\\n globalThis.view = start({\\n annotations,\\n storyEntries: normalizedStories,\\n\\n });\\n} else {\\n updateView(globalThis.view, annotations, normalizedStories);\\n}\\n\\nexport const view: View = globalThis.view;\\n"
+"/* do not change this file, it is auto generated by storybook. */\\n/// \\nimport { start, updateView, View, type Features } from '@storybook/react-native';\\n\\n\\nimport \\"@storybook/addon-ondevice-notes/register\\";\\nimport \\"@storybook/addon-ondevice-controls/register\\";\\nimport \\"@storybook/addon-ondevice-backgrounds/register\\";\\nimport \\"@storybook/addon-ondevice-actions/register\\";\\n\\nconst normalizedStories = [\\n {\\n titlePrefix: \\"\\",\\n directory: \\"./scripts/mocks/all-config-files\\",\\n files: \\"FakeStory.stories.tsx\\",\\n importPathMatcher: /^\\\\.[\\\\\\\\/](?:FakeStory\\\\.stories\\\\.tsx)$/,\\n req: require.context(\\n './',\\n false,\\n /^\\\\.[\\\\\\\\/](?:FakeStory\\\\.stories\\\\.tsx)$/\\n ),\\n }\\n];\\n\\n\\ndeclare global {\\n var view: View;\\n var STORIES: typeof normalizedStories;\\n var STORYBOOK_WEBSOCKET:\\n | { host?: string; port?: number; secured?: boolean }\\n | undefined;\\n var FEATURES: Features;\\n}\\n\\n\\nconst annotations = [\\n require('./preview'),\\n require(\\"@storybook/react-native/preview\\")\\n];\\n\\nglobalThis.STORIES = normalizedStories;\\nglobalThis.STORYBOOK_WEBSOCKET = {\\n host: 'localhost',\\n port: 7007,\\n secured: false,\\n};\\n\\nmodule?.hot?.accept?.();\\n\\nconst options = {}\\n\\nif (!globalThis.view) {\\n globalThis.view = start({\\n annotations,\\n storyEntries: normalizedStories,\\n options,\\n });\\n} else {\\n updateView(globalThis.view, annotations, normalizedStories, options);\\n}\\n\\nexport const view: View = globalThis.view;\\n"
`;
exports[`loader > writeRequires > when only port is provided without host > includes STORYBOOK_WEBSOCKET with port and secured flag 1`] = `
-"/* do not change this file, it is auto generated by storybook. */\\n/// \\nimport { start, updateView, View, type Features } from '@storybook/react-native';\\n\\nimport \\"@storybook/addon-ondevice-notes/register\\";\\nimport \\"@storybook/addon-ondevice-controls/register\\";\\nimport \\"@storybook/addon-ondevice-backgrounds/register\\";\\nimport \\"@storybook/addon-ondevice-actions/register\\";\\n\\nconst normalizedStories = [\\n {\\n titlePrefix: \\"\\",\\n directory: \\"./scripts/mocks/all-config-files\\",\\n files: \\"FakeStory.stories.tsx\\",\\n importPathMatcher: /^\\\\.[\\\\\\\\/](?:FakeStory\\\\.stories\\\\.tsx)$/,\\n req: require.context(\\n './',\\n false,\\n /^\\\\.[\\\\\\\\/](?:FakeStory\\\\.stories\\\\.tsx)$/\\n ),\\n }\\n];\\n\\n\\ndeclare global {\\n var view: View;\\n var STORIES: typeof normalizedStories;\\n var STORYBOOK_WEBSOCKET:\\n | { host?: string; port?: number; secured?: boolean }\\n | undefined;\\n var FEATURES: Features;\\n}\\n\\n\\nconst annotations = [\\n require('./preview'),\\n require(\\"@storybook/react-native/preview\\")\\n];\\n\\nglobalThis.STORIES = normalizedStories;\\nglobalThis.STORYBOOK_WEBSOCKET = {\\n port: 8080,\\n secured: false,\\n};\\n\\nmodule?.hot?.accept?.();\\n\\n\\n\\nif (!globalThis.view) {\\n globalThis.view = start({\\n annotations,\\n storyEntries: normalizedStories,\\n\\n });\\n} else {\\n updateView(globalThis.view, annotations, normalizedStories);\\n}\\n\\nexport const view: View = globalThis.view;\\n"
+"/* do not change this file, it is auto generated by storybook. */\\n/// \\nimport { start, updateView, View, type Features } from '@storybook/react-native';\\n\\n\\nimport \\"@storybook/addon-ondevice-notes/register\\";\\nimport \\"@storybook/addon-ondevice-controls/register\\";\\nimport \\"@storybook/addon-ondevice-backgrounds/register\\";\\nimport \\"@storybook/addon-ondevice-actions/register\\";\\n\\nconst normalizedStories = [\\n {\\n titlePrefix: \\"\\",\\n directory: \\"./scripts/mocks/all-config-files\\",\\n files: \\"FakeStory.stories.tsx\\",\\n importPathMatcher: /^\\\\.[\\\\\\\\/](?:FakeStory\\\\.stories\\\\.tsx)$/,\\n req: require.context(\\n './',\\n false,\\n /^\\\\.[\\\\\\\\/](?:FakeStory\\\\.stories\\\\.tsx)$/\\n ),\\n }\\n];\\n\\n\\ndeclare global {\\n var view: View;\\n var STORIES: typeof normalizedStories;\\n var STORYBOOK_WEBSOCKET:\\n | { host?: string; port?: number; secured?: boolean }\\n | undefined;\\n var FEATURES: Features;\\n}\\n\\n\\nconst annotations = [\\n require('./preview'),\\n require(\\"@storybook/react-native/preview\\")\\n];\\n\\nglobalThis.STORIES = normalizedStories;\\nglobalThis.STORYBOOK_WEBSOCKET = {\\n port: 8080,\\n secured: false,\\n};\\n\\nmodule?.hot?.accept?.();\\n\\nconst options = {}\\n\\nif (!globalThis.view) {\\n globalThis.view = start({\\n annotations,\\n storyEntries: normalizedStories,\\n options,\\n });\\n} else {\\n updateView(globalThis.view, annotations, normalizedStories, options);\\n}\\n\\nexport const view: View = globalThis.view;\\n"
`;
exports[`loader > writeRequires > when the main config is a cjs file > writes the story imports 1`] = `
-"/* do not change this file, it is auto generated by storybook. */\\n/// \\nimport { start, updateView, View, type Features } from '@storybook/react-native';\\n\\nimport \\"@storybook/addon-ondevice-notes/register\\";\\nimport \\"@storybook/addon-ondevice-controls/register\\";\\nimport \\"@storybook/addon-ondevice-backgrounds/register\\";\\nimport \\"@storybook/addon-ondevice-actions/register\\";\\n\\nconst normalizedStories = [\\n {\\n titlePrefix: \\"\\",\\n directory: \\"./scripts/mocks/cjs-config\\",\\n files: \\"FakeStory.stories.tsx\\",\\n importPathMatcher: /^\\\\.[\\\\\\\\/](?:FakeStory\\\\.stories\\\\.tsx)$/,\\n req: require.context(\\n './',\\n false,\\n /^\\\\.[\\\\\\\\/](?:FakeStory\\\\.stories\\\\.tsx)$/\\n ),\\n }\\n];\\n\\n\\ndeclare global {\\n var view: View;\\n var STORIES: typeof normalizedStories;\\n var STORYBOOK_WEBSOCKET:\\n | { host?: string; port?: number; secured?: boolean }\\n | undefined;\\n var FEATURES: Features;\\n}\\n\\n\\nconst annotations = [\\n require(\\"@storybook/react-native/preview\\")\\n];\\n\\nglobalThis.STORIES = normalizedStories;\\n\\n\\nmodule?.hot?.accept?.();\\n\\n\\n\\nif (!globalThis.view) {\\n globalThis.view = start({\\n annotations,\\n storyEntries: normalizedStories,\\n\\n });\\n} else {\\n updateView(globalThis.view, annotations, normalizedStories);\\n}\\n\\nexport const view: View = globalThis.view;\\n"
+"/* do not change this file, it is auto generated by storybook. */\\n/// \\nimport { start, updateView, View, type Features } from '@storybook/react-native';\\n\\n\\nimport \\"@storybook/addon-ondevice-notes/register\\";\\nimport \\"@storybook/addon-ondevice-controls/register\\";\\nimport \\"@storybook/addon-ondevice-backgrounds/register\\";\\nimport \\"@storybook/addon-ondevice-actions/register\\";\\n\\nconst normalizedStories = [\\n {\\n titlePrefix: \\"\\",\\n directory: \\"./scripts/mocks/cjs-config\\",\\n files: \\"FakeStory.stories.tsx\\",\\n importPathMatcher: /^\\\\.[\\\\\\\\/](?:FakeStory\\\\.stories\\\\.tsx)$/,\\n req: require.context(\\n './',\\n false,\\n /^\\\\.[\\\\\\\\/](?:FakeStory\\\\.stories\\\\.tsx)$/\\n ),\\n }\\n];\\n\\n\\ndeclare global {\\n var view: View;\\n var STORIES: typeof normalizedStories;\\n var STORYBOOK_WEBSOCKET:\\n | { host?: string; port?: number; secured?: boolean }\\n | undefined;\\n var FEATURES: Features;\\n}\\n\\n\\nconst annotations = [\\n require(\\"@storybook/react-native/preview\\")\\n];\\n\\nglobalThis.STORIES = normalizedStories;\\n\\n\\nmodule?.hot?.accept?.();\\n\\nconst options = {}\\n\\nif (!globalThis.view) {\\n globalThis.view = start({\\n annotations,\\n storyEntries: normalizedStories,\\n options,\\n });\\n} else {\\n updateView(globalThis.view, annotations, normalizedStories, options);\\n}\\n\\nexport const view: View = globalThis.view;\\n"
`;
exports[`loader > writeRequires > when there are different file extensions > writes the story imports 1`] = `
-"/* do not change this file, it is auto generated by storybook. */\\n/// \\nimport { start, updateView, View, type Features } from '@storybook/react-native';\\n\\nimport \\"@storybook/addon-ondevice-notes/register\\";\\nimport \\"@storybook/addon-ondevice-controls/register\\";\\nimport \\"@storybook/addon-ondevice-backgrounds/register\\";\\nimport \\"@storybook/addon-ondevice-actions/register\\";\\n\\nconst normalizedStories = [\\n {\\n titlePrefix: \\"\\",\\n directory: \\"./scripts/mocks/file-extensions\\",\\n files: \\"FakeStory.stories.tsx\\",\\n importPathMatcher: /^\\\\.[\\\\\\\\/](?:FakeStory\\\\.stories\\\\.tsx)$/,\\n req: require.context(\\n './',\\n false,\\n /^\\\\.[\\\\\\\\/](?:FakeStory\\\\.stories\\\\.tsx)$/\\n ),\\n }\\n];\\n\\n\\ndeclare global {\\n var view: View;\\n var STORIES: typeof normalizedStories;\\n var STORYBOOK_WEBSOCKET:\\n | { host?: string; port?: number; secured?: boolean }\\n | undefined;\\n var FEATURES: Features;\\n}\\n\\n\\nconst annotations = [\\n require('./preview'),\\n require(\\"@storybook/react-native/preview\\")\\n];\\n\\nglobalThis.STORIES = normalizedStories;\\n\\n\\nmodule?.hot?.accept?.();\\n\\n\\n\\nif (!globalThis.view) {\\n globalThis.view = start({\\n annotations,\\n storyEntries: normalizedStories,\\n\\n });\\n} else {\\n updateView(globalThis.view, annotations, normalizedStories);\\n}\\n\\nexport const view: View = globalThis.view;\\n"
+"/* do not change this file, it is auto generated by storybook. */\\n/// \\nimport { start, updateView, View, type Features } from '@storybook/react-native';\\n\\n\\nimport \\"@storybook/addon-ondevice-notes/register\\";\\nimport \\"@storybook/addon-ondevice-controls/register\\";\\nimport \\"@storybook/addon-ondevice-backgrounds/register\\";\\nimport \\"@storybook/addon-ondevice-actions/register\\";\\n\\nconst normalizedStories = [\\n {\\n titlePrefix: \\"\\",\\n directory: \\"./scripts/mocks/file-extensions\\",\\n files: \\"FakeStory.stories.tsx\\",\\n importPathMatcher: /^\\\\.[\\\\\\\\/](?:FakeStory\\\\.stories\\\\.tsx)$/,\\n req: require.context(\\n './',\\n false,\\n /^\\\\.[\\\\\\\\/](?:FakeStory\\\\.stories\\\\.tsx)$/\\n ),\\n }\\n];\\n\\n\\ndeclare global {\\n var view: View;\\n var STORIES: typeof normalizedStories;\\n var STORYBOOK_WEBSOCKET:\\n | { host?: string; port?: number; secured?: boolean }\\n | undefined;\\n var FEATURES: Features;\\n}\\n\\n\\nconst annotations = [\\n require('./preview'),\\n require(\\"@storybook/react-native/preview\\")\\n];\\n\\nglobalThis.STORIES = normalizedStories;\\n\\n\\nmodule?.hot?.accept?.();\\n\\nconst options = {}\\n\\nif (!globalThis.view) {\\n globalThis.view = start({\\n annotations,\\n storyEntries: normalizedStories,\\n options,\\n });\\n} else {\\n updateView(globalThis.view, annotations, normalizedStories, options);\\n}\\n\\nexport const view: View = globalThis.view;\\n"
`;
exports[`loader > writeRequires > when there is a configuration object > writes the story imports 1`] = `
-"/* do not change this file, it is auto generated by storybook. */\\n/// \\nimport { start, updateView, View, type Features } from '@storybook/react-native';\\n\\nimport \\"@storybook/addon-ondevice-notes/register\\";\\nimport \\"@storybook/addon-ondevice-controls/register\\";\\nimport \\"@storybook/addon-ondevice-backgrounds/register\\";\\nimport \\"@storybook/addon-ondevice-actions/register\\";\\n\\nconst normalizedStories = [\\n {\\n titlePrefix: \\"ComponentsPrefix\\",\\n directory: \\"./scripts/mocks/configuration-objects/components\\",\\n files: \\"**/*.stories.tsx\\",\\n importPathMatcher: /^\\\\.(?:(?:^|\\\\/|(?:(?:(?!(?:^|\\\\/)\\\\.).)*?)\\\\/)(?!\\\\.)(?=.)[^/]*?\\\\.stories\\\\.tsx)$/,\\n req: require.context(\\n './components',\\n true,\\n /^\\\\.(?:(?:^|\\\\/|(?:(?:(?!(?:^|\\\\/)\\\\.).)*?)\\\\/)(?!\\\\.)(?=.)[^/]*?\\\\.stories\\\\.tsx)$/\\n ),\\n }\\n];\\n\\n\\ndeclare global {\\n var view: View;\\n var STORIES: typeof normalizedStories;\\n var STORYBOOK_WEBSOCKET:\\n | { host?: string; port?: number; secured?: boolean }\\n | undefined;\\n var FEATURES: Features;\\n}\\n\\n\\nconst annotations = [\\n require('./preview'),\\n require(\\"@storybook/react-native/preview\\")\\n];\\n\\nglobalThis.STORIES = normalizedStories;\\n\\n\\nmodule?.hot?.accept?.();\\n\\n\\n\\nif (!globalThis.view) {\\n globalThis.view = start({\\n annotations,\\n storyEntries: normalizedStories,\\n\\n });\\n} else {\\n updateView(globalThis.view, annotations, normalizedStories);\\n}\\n\\nexport const view: View = globalThis.view;\\n"
+"/* do not change this file, it is auto generated by storybook. */\\n/// \\nimport { start, updateView, View, type Features } from '@storybook/react-native';\\n\\n\\nimport \\"@storybook/addon-ondevice-notes/register\\";\\nimport \\"@storybook/addon-ondevice-controls/register\\";\\nimport \\"@storybook/addon-ondevice-backgrounds/register\\";\\nimport \\"@storybook/addon-ondevice-actions/register\\";\\n\\nconst normalizedStories = [\\n {\\n titlePrefix: \\"ComponentsPrefix\\",\\n directory: \\"./scripts/mocks/configuration-objects/components\\",\\n files: \\"**/*.stories.tsx\\",\\n importPathMatcher: /^\\\\.(?:(?:^|\\\\/|(?:(?:(?!(?:^|\\\\/)\\\\.).)*?)\\\\/)(?!\\\\.)(?=.)[^/]*?\\\\.stories\\\\.tsx)$/,\\n req: require.context(\\n './components',\\n true,\\n /^\\\\.(?:(?:^|\\\\/|(?:(?:(?!(?:^|\\\\/)\\\\.).)*?)\\\\/)(?!\\\\.)(?=.)[^/]*?\\\\.stories\\\\.tsx)$/\\n ),\\n }\\n];\\n\\n\\ndeclare global {\\n var view: View;\\n var STORIES: typeof normalizedStories;\\n var STORYBOOK_WEBSOCKET:\\n | { host?: string; port?: number; secured?: boolean }\\n | undefined;\\n var FEATURES: Features;\\n}\\n\\n\\nconst annotations = [\\n require('./preview'),\\n require(\\"@storybook/react-native/preview\\")\\n];\\n\\nglobalThis.STORIES = normalizedStories;\\n\\n\\nmodule?.hot?.accept?.();\\n\\nconst options = {}\\n\\nif (!globalThis.view) {\\n globalThis.view = start({\\n annotations,\\n storyEntries: normalizedStories,\\n options,\\n });\\n} else {\\n updateView(globalThis.view, annotations, normalizedStories, options);\\n}\\n\\nexport const view: View = globalThis.view;\\n"
`;
exports[`loader > writeRequires > when there is a story glob > writes the story imports 1`] = `
-"/* do not change this file, it is auto generated by storybook. */\\n/// \\nimport { start, updateView, View, type Features } from '@storybook/react-native';\\n\\nimport \\"@storybook/addon-ondevice-notes/register\\";\\nimport \\"@storybook/addon-ondevice-controls/register\\";\\nimport \\"@storybook/addon-ondevice-backgrounds/register\\";\\nimport \\"@storybook/addon-ondevice-actions/register\\";\\n\\nconst normalizedStories = [\\n {\\n titlePrefix: \\"\\",\\n directory: \\"./scripts/mocks/all-config-files\\",\\n files: \\"FakeStory.stories.tsx\\",\\n importPathMatcher: /^\\\\.[\\\\\\\\/](?:FakeStory\\\\.stories\\\\.tsx)$/,\\n req: require.context(\\n './',\\n false,\\n /^\\\\.[\\\\\\\\/](?:FakeStory\\\\.stories\\\\.tsx)$/\\n ),\\n }\\n];\\n\\n\\ndeclare global {\\n var view: View;\\n var STORIES: typeof normalizedStories;\\n var STORYBOOK_WEBSOCKET:\\n | { host?: string; port?: number; secured?: boolean }\\n | undefined;\\n var FEATURES: Features;\\n}\\n\\n\\nconst annotations = [\\n require('./preview'),\\n require(\\"@storybook/react-native/preview\\")\\n];\\n\\nglobalThis.STORIES = normalizedStories;\\n\\n\\nmodule?.hot?.accept?.();\\n\\n\\n\\nif (!globalThis.view) {\\n globalThis.view = start({\\n annotations,\\n storyEntries: normalizedStories,\\n\\n });\\n} else {\\n updateView(globalThis.view, annotations, normalizedStories);\\n}\\n\\nexport const view: View = globalThis.view;\\n"
+"/* do not change this file, it is auto generated by storybook. */\\n/// \\nimport { start, updateView, View, type Features } from '@storybook/react-native';\\n\\n\\nimport \\"@storybook/addon-ondevice-notes/register\\";\\nimport \\"@storybook/addon-ondevice-controls/register\\";\\nimport \\"@storybook/addon-ondevice-backgrounds/register\\";\\nimport \\"@storybook/addon-ondevice-actions/register\\";\\n\\nconst normalizedStories = [\\n {\\n titlePrefix: \\"\\",\\n directory: \\"./scripts/mocks/all-config-files\\",\\n files: \\"FakeStory.stories.tsx\\",\\n importPathMatcher: /^\\\\.[\\\\\\\\/](?:FakeStory\\\\.stories\\\\.tsx)$/,\\n req: require.context(\\n './',\\n false,\\n /^\\\\.[\\\\\\\\/](?:FakeStory\\\\.stories\\\\.tsx)$/\\n ),\\n }\\n];\\n\\n\\ndeclare global {\\n var view: View;\\n var STORIES: typeof normalizedStories;\\n var STORYBOOK_WEBSOCKET:\\n | { host?: string; port?: number; secured?: boolean }\\n | undefined;\\n var FEATURES: Features;\\n}\\n\\n\\nconst annotations = [\\n require('./preview'),\\n require(\\"@storybook/react-native/preview\\")\\n];\\n\\nglobalThis.STORIES = normalizedStories;\\n\\n\\nmodule?.hot?.accept?.();\\n\\nconst options = {}\\n\\nif (!globalThis.view) {\\n globalThis.view = start({\\n annotations,\\n storyEntries: normalizedStories,\\n options,\\n });\\n} else {\\n updateView(globalThis.view, annotations, normalizedStories, options);\\n}\\n\\nexport const view: View = globalThis.view;\\n"
`;
exports[`loader > writeRequires > when there is no preview > does not add preview related stuff 1`] = `
-"/* do not change this file, it is auto generated by storybook. */\\n/// \\nimport { start, updateView, View, type Features } from '@storybook/react-native';\\n\\nimport \\"@storybook/addon-ondevice-notes/register\\";\\nimport \\"@storybook/addon-ondevice-controls/register\\";\\nimport \\"@storybook/addon-ondevice-backgrounds/register\\";\\nimport \\"@storybook/addon-ondevice-actions/register\\";\\n\\nconst normalizedStories = [\\n {\\n titlePrefix: \\"\\",\\n directory: \\"./scripts/mocks/no-preview\\",\\n files: \\"FakeStory.stories.tsx\\",\\n importPathMatcher: /^\\\\.[\\\\\\\\/](?:FakeStory\\\\.stories\\\\.tsx)$/,\\n req: require.context(\\n './',\\n false,\\n /^\\\\.[\\\\\\\\/](?:FakeStory\\\\.stories\\\\.tsx)$/\\n ),\\n }\\n];\\n\\n\\ndeclare global {\\n var view: View;\\n var STORIES: typeof normalizedStories;\\n var STORYBOOK_WEBSOCKET:\\n | { host?: string; port?: number; secured?: boolean }\\n | undefined;\\n var FEATURES: Features;\\n}\\n\\n\\nconst annotations = [\\n require(\\"@storybook/react-native/preview\\")\\n];\\n\\nglobalThis.STORIES = normalizedStories;\\n\\n\\nmodule?.hot?.accept?.();\\n\\n\\n\\nif (!globalThis.view) {\\n globalThis.view = start({\\n annotations,\\n storyEntries: normalizedStories,\\n\\n });\\n} else {\\n updateView(globalThis.view, annotations, normalizedStories);\\n}\\n\\nexport const view: View = globalThis.view;\\n"
+"/* do not change this file, it is auto generated by storybook. */\\n/// \\nimport { start, updateView, View, type Features } from '@storybook/react-native';\\n\\n\\nimport \\"@storybook/addon-ondevice-notes/register\\";\\nimport \\"@storybook/addon-ondevice-controls/register\\";\\nimport \\"@storybook/addon-ondevice-backgrounds/register\\";\\nimport \\"@storybook/addon-ondevice-actions/register\\";\\n\\nconst normalizedStories = [\\n {\\n titlePrefix: \\"\\",\\n directory: \\"./scripts/mocks/no-preview\\",\\n files: \\"FakeStory.stories.tsx\\",\\n importPathMatcher: /^\\\\.[\\\\\\\\/](?:FakeStory\\\\.stories\\\\.tsx)$/,\\n req: require.context(\\n './',\\n false,\\n /^\\\\.[\\\\\\\\/](?:FakeStory\\\\.stories\\\\.tsx)$/\\n ),\\n }\\n];\\n\\n\\ndeclare global {\\n var view: View;\\n var STORIES: typeof normalizedStories;\\n var STORYBOOK_WEBSOCKET:\\n | { host?: string; port?: number; secured?: boolean }\\n | undefined;\\n var FEATURES: Features;\\n}\\n\\n\\nconst annotations = [\\n require(\\"@storybook/react-native/preview\\")\\n];\\n\\nglobalThis.STORIES = normalizedStories;\\n\\n\\nmodule?.hot?.accept?.();\\n\\nconst options = {}\\n\\nif (!globalThis.view) {\\n globalThis.view = start({\\n annotations,\\n storyEntries: normalizedStories,\\n options,\\n });\\n} else {\\n updateView(globalThis.view, annotations, normalizedStories, options);\\n}\\n\\nexport const view: View = globalThis.view;\\n"
`;
exports[`loader > writeRequires > when using js > writes the story imports without types 1`] = `
-"/* do not change this file, it is auto generated by storybook. */\\nimport { start, updateView } from '@storybook/react-native';\\n\\nimport \\"@storybook/addon-ondevice-notes/register\\";\\nimport \\"@storybook/addon-ondevice-controls/register\\";\\nimport \\"@storybook/addon-ondevice-backgrounds/register\\";\\nimport \\"@storybook/addon-ondevice-actions/register\\";\\n\\nconst normalizedStories = [\\n {\\n titlePrefix: \\"\\",\\n directory: \\"./scripts/mocks/all-config-files\\",\\n files: \\"FakeStory.stories.tsx\\",\\n importPathMatcher: /^\\\\.[\\\\\\\\/](?:FakeStory\\\\.stories\\\\.tsx)$/,\\n req: require.context(\\n './',\\n false,\\n /^\\\\.[\\\\\\\\/](?:FakeStory\\\\.stories\\\\.tsx)$/\\n ),\\n }\\n];\\n\\n\\n\\nconst annotations = [\\n require('./preview'),\\n require(\\"@storybook/react-native/preview\\")\\n];\\n\\nglobalThis.STORIES = normalizedStories;\\n\\n\\nmodule?.hot?.accept?.();\\n\\n\\n\\nif (!globalThis.view) {\\n globalThis.view = start({\\n annotations,\\n storyEntries: normalizedStories,\\n\\n });\\n} else {\\n updateView(globalThis.view, annotations, normalizedStories);\\n}\\n\\nexport const view = globalThis.view;\\n"
+"/* do not change this file, it is auto generated by storybook. */\\nimport { start, updateView } from '@storybook/react-native';\\n\\n\\nimport \\"@storybook/addon-ondevice-notes/register\\";\\nimport \\"@storybook/addon-ondevice-controls/register\\";\\nimport \\"@storybook/addon-ondevice-backgrounds/register\\";\\nimport \\"@storybook/addon-ondevice-actions/register\\";\\n\\nconst normalizedStories = [\\n {\\n titlePrefix: \\"\\",\\n directory: \\"./scripts/mocks/all-config-files\\",\\n files: \\"FakeStory.stories.tsx\\",\\n importPathMatcher: /^\\\\.[\\\\\\\\/](?:FakeStory\\\\.stories\\\\.tsx)$/,\\n req: require.context(\\n './',\\n false,\\n /^\\\\.[\\\\\\\\/](?:FakeStory\\\\.stories\\\\.tsx)$/\\n ),\\n }\\n];\\n\\n\\n\\nconst annotations = [\\n require('./preview'),\\n require(\\"@storybook/react-native/preview\\")\\n];\\n\\nglobalThis.STORIES = normalizedStories;\\n\\n\\nmodule?.hot?.accept?.();\\n\\nconst options = {}\\n\\nif (!globalThis.view) {\\n globalThis.view = start({\\n annotations,\\n storyEntries: normalizedStories,\\n options,\\n });\\n} else {\\n updateView(globalThis.view, annotations, normalizedStories, options);\\n}\\n\\nexport const view = globalThis.view;\\n"
`;
diff --git a/tests/scripts/mocks/all-config-files/main.js b/tests/scripts/mocks/all-config-files/main.js
index 0e61140c8f..3725e576b9 100644
--- a/tests/scripts/mocks/all-config-files/main.js
+++ b/tests/scripts/mocks/all-config-files/main.js
@@ -1,6 +1,6 @@
export default {
stories: ['./FakeStory.stories.tsx'],
- addons: [
+ deviceAddons: [
'@storybook/addon-ondevice-notes',
'@storybook/addon-ondevice-controls',
'@storybook/addon-ondevice-backgrounds',
diff --git a/tests/scripts/mocks/cjs-config/main.cjs b/tests/scripts/mocks/cjs-config/main.cjs
index 1240782e8d..89592ace58 100644
--- a/tests/scripts/mocks/cjs-config/main.cjs
+++ b/tests/scripts/mocks/cjs-config/main.cjs
@@ -1,6 +1,6 @@
module.exports = {
stories: ['./FakeStory.stories.tsx'],
- addons: [
+ deviceAddons: [
'@storybook/addon-ondevice-notes',
'@storybook/addon-ondevice-controls',
'@storybook/addon-ondevice-backgrounds',
diff --git a/tests/scripts/mocks/configuration-objects/main.js b/tests/scripts/mocks/configuration-objects/main.js
index 656091819a..fc5dc3a3fa 100644
--- a/tests/scripts/mocks/configuration-objects/main.js
+++ b/tests/scripts/mocks/configuration-objects/main.js
@@ -6,7 +6,7 @@ export default {
titlePrefix: 'ComponentsPrefix',
},
],
- addons: [
+ deviceAddons: [
'@storybook/addon-ondevice-notes',
'@storybook/addon-ondevice-controls',
'@storybook/addon-ondevice-backgrounds',
diff --git a/tests/scripts/mocks/device-addons/FakeComponent.tsx b/tests/scripts/mocks/device-addons/FakeComponent.tsx
new file mode 100644
index 0000000000..23915b0493
--- /dev/null
+++ b/tests/scripts/mocks/device-addons/FakeComponent.tsx
@@ -0,0 +1 @@
+export const FakeComponent = () => null;
diff --git a/tests/scripts/mocks/device-addons/FakeStory.stories.tsx b/tests/scripts/mocks/device-addons/FakeStory.stories.tsx
new file mode 100644
index 0000000000..ca6d412600
--- /dev/null
+++ b/tests/scripts/mocks/device-addons/FakeStory.stories.tsx
@@ -0,0 +1,10 @@
+import { FakeComponent } from './FakeComponent';
+
+export default {
+ title: 'components/FakeComponent',
+ component: FakeComponent,
+};
+
+export const Basic = {
+ args: {},
+};
diff --git a/tests/scripts/mocks/device-addons/main.js b/tests/scripts/mocks/device-addons/main.js
new file mode 100644
index 0000000000..3725e576b9
--- /dev/null
+++ b/tests/scripts/mocks/device-addons/main.js
@@ -0,0 +1,9 @@
+export default {
+ stories: ['./FakeStory.stories.tsx'],
+ deviceAddons: [
+ '@storybook/addon-ondevice-notes',
+ '@storybook/addon-ondevice-controls',
+ '@storybook/addon-ondevice-backgrounds',
+ '@storybook/addon-ondevice-actions',
+ ],
+};
diff --git a/tests/scripts/mocks/device-addons/preview.js b/tests/scripts/mocks/device-addons/preview.js
new file mode 100644
index 0000000000..13bad13073
--- /dev/null
+++ b/tests/scripts/mocks/device-addons/preview.js
@@ -0,0 +1,24 @@
+import React from 'react';
+import { View, StyleSheet } from 'react-native';
+import { withBackgrounds } from '@storybook/addon-ondevice-backgrounds';
+
+export const decorators = [
+ (StoryFn) => (
+
+
+
+ ),
+ withBackgrounds,
+];
+export const parameters = {
+ my_param: 'anything',
+ backgrounds: [
+ { name: 'plain', value: 'white', default: true },
+ { name: 'warm', value: 'hotpink' },
+ { name: 'cool', value: 'deepskyblue' },
+ ],
+};
+
+const styles = StyleSheet.create({
+ container: { padding: 8, flex: 1 },
+});
diff --git a/tests/scripts/mocks/exclude-config-files/main.js b/tests/scripts/mocks/exclude-config-files/main.js
index 13cc36b19f..751bfe5da2 100644
--- a/tests/scripts/mocks/exclude-config-files/main.js
+++ b/tests/scripts/mocks/exclude-config-files/main.js
@@ -3,7 +3,7 @@ export default {
reactNativeOptions: {
excludePaths: '**/exclude-components/**',
},
- addons: [
+ deviceAddons: [
'@storybook/addon-ondevice-notes',
'@storybook/addon-ondevice-controls',
'@storybook/addon-ondevice-backgrounds',
diff --git a/tests/scripts/mocks/file-extensions/main.ts b/tests/scripts/mocks/file-extensions/main.ts
index f0c6868541..247675708d 100644
--- a/tests/scripts/mocks/file-extensions/main.ts
+++ b/tests/scripts/mocks/file-extensions/main.ts
@@ -1,6 +1,6 @@
const config = {
stories: ['./FakeStory.stories.tsx'],
- addons: [
+ deviceAddons: [
'@storybook/addon-ondevice-notes',
'@storybook/addon-ondevice-controls',
'@storybook/addon-ondevice-backgrounds',
diff --git a/tests/scripts/mocks/legacy-ondevice-in-addons/FakeComponent.tsx b/tests/scripts/mocks/legacy-ondevice-in-addons/FakeComponent.tsx
new file mode 100644
index 0000000000..6229f9879c
--- /dev/null
+++ b/tests/scripts/mocks/legacy-ondevice-in-addons/FakeComponent.tsx
@@ -0,0 +1,6 @@
+import React from 'react';
+import { Text } from 'react-native';
+
+export function FakeComponent() {
+ return Fake;
+}
diff --git a/tests/scripts/mocks/legacy-ondevice-in-addons/FakeStory.stories.tsx b/tests/scripts/mocks/legacy-ondevice-in-addons/FakeStory.stories.tsx
new file mode 100644
index 0000000000..ca6d412600
--- /dev/null
+++ b/tests/scripts/mocks/legacy-ondevice-in-addons/FakeStory.stories.tsx
@@ -0,0 +1,10 @@
+import { FakeComponent } from './FakeComponent';
+
+export default {
+ title: 'components/FakeComponent',
+ component: FakeComponent,
+};
+
+export const Basic = {
+ args: {},
+};
diff --git a/tests/scripts/mocks/legacy-ondevice-in-addons/main.js b/tests/scripts/mocks/legacy-ondevice-in-addons/main.js
new file mode 100644
index 0000000000..2e489bbded
--- /dev/null
+++ b/tests/scripts/mocks/legacy-ondevice-in-addons/main.js
@@ -0,0 +1,4 @@
+export default {
+ stories: ['./FakeStory.stories.tsx'],
+ addons: ['@storybook/addon-ondevice-controls'],
+};
diff --git a/tests/scripts/mocks/mixed-addons/FakeComponent.tsx b/tests/scripts/mocks/mixed-addons/FakeComponent.tsx
new file mode 100644
index 0000000000..23915b0493
--- /dev/null
+++ b/tests/scripts/mocks/mixed-addons/FakeComponent.tsx
@@ -0,0 +1 @@
+export const FakeComponent = () => null;
diff --git a/tests/scripts/mocks/mixed-addons/FakeStory.stories.tsx b/tests/scripts/mocks/mixed-addons/FakeStory.stories.tsx
new file mode 100644
index 0000000000..ca6d412600
--- /dev/null
+++ b/tests/scripts/mocks/mixed-addons/FakeStory.stories.tsx
@@ -0,0 +1,10 @@
+import { FakeComponent } from './FakeComponent';
+
+export default {
+ title: 'components/FakeComponent',
+ component: FakeComponent,
+};
+
+export const Basic = {
+ args: {},
+};
diff --git a/tests/scripts/mocks/mixed-addons/main.js b/tests/scripts/mocks/mixed-addons/main.js
new file mode 100644
index 0000000000..668d4c1837
--- /dev/null
+++ b/tests/scripts/mocks/mixed-addons/main.js
@@ -0,0 +1,10 @@
+export default {
+ stories: ['./FakeStory.stories.tsx'],
+ addons: ['__storybook_generate_test_nonexistent_addon__'],
+ deviceAddons: [
+ '@storybook/addon-ondevice-notes',
+ '@storybook/addon-ondevice-controls',
+ '@storybook/addon-ondevice-backgrounds',
+ '@storybook/addon-ondevice-actions',
+ ],
+};
diff --git a/tests/scripts/mocks/mixed-addons/preview.js b/tests/scripts/mocks/mixed-addons/preview.js
new file mode 100644
index 0000000000..13bad13073
--- /dev/null
+++ b/tests/scripts/mocks/mixed-addons/preview.js
@@ -0,0 +1,24 @@
+import React from 'react';
+import { View, StyleSheet } from 'react-native';
+import { withBackgrounds } from '@storybook/addon-ondevice-backgrounds';
+
+export const decorators = [
+ (StoryFn) => (
+
+
+
+ ),
+ withBackgrounds,
+];
+export const parameters = {
+ my_param: 'anything',
+ backgrounds: [
+ { name: 'plain', value: 'white', default: true },
+ { name: 'warm', value: 'hotpink' },
+ { name: 'cool', value: 'deepskyblue' },
+ ],
+};
+
+const styles = StyleSheet.create({
+ container: { padding: 8, flex: 1 },
+});
diff --git a/tests/scripts/mocks/no-preview/main.js b/tests/scripts/mocks/no-preview/main.js
index 0e61140c8f..3725e576b9 100644
--- a/tests/scripts/mocks/no-preview/main.js
+++ b/tests/scripts/mocks/no-preview/main.js
@@ -1,6 +1,6 @@
export default {
stories: ['./FakeStory.stories.tsx'],
- addons: [
+ deviceAddons: [
'@storybook/addon-ondevice-notes',
'@storybook/addon-ondevice-controls',
'@storybook/addon-ondevice-backgrounds',
diff --git a/tests/scripts/mocks/with-features/main.js b/tests/scripts/mocks/with-features/main.js
index 3e50b63a31..c44ef3fdc8 100644
--- a/tests/scripts/mocks/with-features/main.js
+++ b/tests/scripts/mocks/with-features/main.js
@@ -1,6 +1,6 @@
export default {
stories: ['./FakeStory.stories.tsx'],
- addons: [
+ deviceAddons: [
'@storybook/addon-ondevice-notes',
'@storybook/addon-ondevice-controls',
'@storybook/addon-ondevice-actions',
diff --git a/tests/scripts/mocks/wrong-extension-preview/main.js b/tests/scripts/mocks/wrong-extension-preview/main.js
index 0e61140c8f..3725e576b9 100644
--- a/tests/scripts/mocks/wrong-extension-preview/main.js
+++ b/tests/scripts/mocks/wrong-extension-preview/main.js
@@ -1,6 +1,6 @@
export default {
stories: ['./FakeStory.stories.tsx'],
- addons: [
+ deviceAddons: [
'@storybook/addon-ondevice-notes',
'@storybook/addon-ondevice-controls',
'@storybook/addon-ondevice-backgrounds',
diff --git a/tsconfig.json b/tsconfig.json
index 9efaffbf6a..6550a312b1 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -3,7 +3,6 @@
"compilerOptions": {
"allowJs": true,
"allowSyntheticDefaultImports": true,
- "baseUrl": ".",
"declaration": true,
"jsx": "react-jsx",
"lib": ["es2022"],