-
-
Notifications
You must be signed in to change notification settings - Fork 180
feat:unified withStorybook wrapper for entrypoint-swapping
#871
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: next
Are you sure you want to change the base?
Changes from all commits
66d541a
a889fb4
537de98
2d4ed00
4d7b128
d115749
7e754f5
a184687
338b2e0
8660d46
513cb9b
b75df62
85ffd43
43edcd8
c9f413d
2a65a27
44dd17c
c3acbc0
597e02e
c861324
969c088
4452d94
ce0fada
9b735ca
986c3db
712ac17
67a4f17
36eb5be
b87dffa
20bf4cb
db9ac2e
a4d3348
c14572f
d586348
34221cc
d6f40b6
5c4dc2b
ed85eb8
cd869db
0d5f722
c29d0eb
ad34914
9f17632
fe68fd7
660fedf
d90278b
97950c4
688d5d9
c3d0b97
9e6cbe3
c821d2e
54a7cd7
04f84a4
1631731
11fba4e
e040a3f
516ff63
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@storybook/react-native': minor | ||
| --- | ||
|
|
||
| Add `deviceAddons` property to `StorybookConfig` for separating on-device addons from core addons. On-device addons listed in `deviceAddons` are only consumed at runtime by the code generator, not evaluated as presets by Storybook Core. This prevents `extract` failures caused by loading React Native code in a Node.js context. Backwards compatible: addons in the `addons` field continue to work. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@storybook/react-native': minor | ||
| --- | ||
|
|
||
| Add unified bundler-agnostic withStorybook wrapper at @storybook/react-native/withStorybook |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| /** | ||
| * Minimal HTTP + WebSocket server for manual connectivity checks (LAN, firewall). | ||
| * Do not bind the same port as Metro's Storybook channel server while Metro is using it. | ||
| * | ||
| * Sends Storybook-style heartbeats every 10s (`{ type: 'ping', args: [] }`), matching | ||
| * packages/react-native/src/metro/channelServer.ts so storybook's WebsocketTransport | ||
| * does not close the socket (~20s) waiting for pings. | ||
| * | ||
| * Env: STORYBOOK_WS_HOST (bind address; omit for all interfaces), STORYBOOK_WS_PORT (default 7007) | ||
| */ | ||
| import { createServer } from 'node:http'; | ||
| import { WebSocketServer } from 'ws'; | ||
|
|
||
| const PING_INTERVAL_MS = 10_000; | ||
|
|
||
| const port = Number(process.env.STORYBOOK_WS_PORT) || 7007; | ||
| const host = process.env.STORYBOOK_WS_HOST || undefined; | ||
|
|
||
| const httpServer = createServer((_req, res) => { | ||
| res.writeHead(404); | ||
| res.end(); | ||
| }); | ||
|
|
||
| const wss = new WebSocketServer({ server: httpServer }); | ||
|
|
||
| // Same global ping interval as createChannelServer — keeps Storybook client transport alive. | ||
| const pingInterval = setInterval(() => { | ||
| wss.clients.forEach((client) => { | ||
| if (client.readyState === WebSocket.OPEN) { | ||
| client.send(JSON.stringify({ type: 'ping', args: [] })); | ||
| } | ||
|
ndelangen marked this conversation as resolved.
|
||
| }); | ||
| }, PING_INTERVAL_MS); | ||
| pingInterval.unref?.(); | ||
|
|
||
| wss.on('connection', (ws) => { | ||
| console.log('[ws-smoke-server] WebSocket connection established'); | ||
|
|
||
| ws.on('message', (data) => { | ||
| const text = data.toString(); | ||
| console.log('[ws-smoke-server] message:', text); | ||
| try { | ||
| const json = JSON.parse(text); | ||
| if (json?.type === 'pong') { | ||
| console.log('[ws-smoke-server] saw Storybook transport pong (heartbeat ack)'); | ||
| } | ||
| } catch { | ||
| // ignore non-JSON | ||
| } | ||
| }); | ||
| }); | ||
|
|
||
| httpServer.listen(port, host, () => { | ||
| const where = host ?? '0.0.0.0 (all interfaces)'; | ||
| console.log(`[ws-smoke-server] listening on ${where}:${port}`); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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<string, PreparedStory<ReactRenderer>> = {}; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| constructor(preview: PreviewWithSelection<ReactRenderer>, channel: Channel) { | ||||||||||||||||||||||||||||
| constructor(preview: PreviewWithSelection<ReactRenderer>, channel: Channel, options: any) { | ||||||||||||||||||||||||||||
| this._preview = preview; | ||||||||||||||||||||||||||||
| this._channel = channel; | ||||||||||||||||||||||||||||
| this._options = options; | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
Comment on lines
+124
to
132
|
||||||||||||||||||||||||||||
| _storyIdExists = (storyId: string) => { | ||||||||||||||||||||||||||||
|
|
@@ -201,13 +206,9 @@ export class View { | |||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| _getServerChannel = (params: Partial<Params> = {}) => { | ||||||||||||||||||||||||||||
| 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<Params> = {}) => { | ||||||||||||||||||||||||||||
| 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); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
Comment on lines
+245
to
+254
|
||||||||||||||||||||||||||||
| 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<ReactRenderer>) => { | ||||||||||||||||||||||||||||
| 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 ( | ||||||||||||||||||||||||||||
| <StoryView useWrapper={storyViewWrapper} storyBackgroundColor={storyBackgroundColor} /> | ||||||||||||||||||||||||||||
| <SafeAreaProvider> | ||||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not sure I understand this part of the change, the ondeviceui is off in this scenario is this intended specifically for the visual testing scenario? sometimes the user would want no safearea here because they have a screen with a background color that fills the full area even on the safe area we have a no safearea parameter maybe it could be a compromise to support that here
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The idea here is that there is 0 storybook UI, no way to navigate, similar to your custom screenshotting view (in fact, that's where I got this from): react-native/examples/expo-example/.rnstorybook/index.tsx Lines 20 to 32 in 74c26e2
The concept is that the user has no way to control the storybook; no UI from the storybook shows at all. You bring an excellent point that some stories users will have should not have I propose we make this configurable in a later iteration, and a parameter seems like the right way to do so. |
||||||||||||||||||||||||||||
| <SafeAreaView style={{ flex: 1 }}> | ||||||||||||||||||||||||||||
| <StatusBar hidden /> | ||||||||||||||||||||||||||||
| <RNView | ||||||||||||||||||||||||||||
| style={{ flex: 1 }} | ||||||||||||||||||||||||||||
| accessibilityLabel={story?.id} | ||||||||||||||||||||||||||||
| testID={story?.id} | ||||||||||||||||||||||||||||
| accessible | ||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||
| <StoryView | ||||||||||||||||||||||||||||
| useWrapper={storyViewWrapper} | ||||||||||||||||||||||||||||
| storyBackgroundColor={storyBackgroundColor} | ||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||
| </RNView> | ||||||||||||||||||||||||||||
| </SafeAreaView> | ||||||||||||||||||||||||||||
| </SafeAreaProvider> | ||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.