-
-
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
Changes from 58 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
7cec271
706b49a
412c3f6
42f7dd5
4030913
2c68fcb
9e9f155
419a6d9
53aa20d
99d09ce
07a8648
d06c3f0
ea0d5c8
4948517
38c5804
9da0fc7
bd8a740
aed734d
2b36ffd
3b2b95b
7960793
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). | ||
|
ndelangen marked this conversation as resolved.
Outdated
|
||
| * 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.
Outdated
ndelangen marked this conversation as resolved.
Outdated
|
||
| }); | ||
| }, 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); | ||
|
ndelangen marked this conversation as resolved.
Outdated
|
||
| const shouldPersistSelection = this._options.disableUI | ||
| ? false | ||
| : (params.shouldPersistSelection ?? true); | ||
|
|
||
|
ndelangen marked this conversation as resolved.
|
||
| 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> | ||
|
ndelangen marked this conversation as resolved.
|
||
| <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.