@storybook/react-native 10.1.3 → 10.2.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/node.js ADDED
@@ -0,0 +1,314 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __commonJS = (cb, mod) => function __require() {
8
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
9
+ };
10
+ var __export = (target, all) => {
11
+ for (var name in all)
12
+ __defProp(target, name, { get: all[name], enumerable: true });
13
+ };
14
+ var __copyProps = (to, from, except, desc) => {
15
+ if (from && typeof from === "object" || typeof from === "function") {
16
+ for (let key of __getOwnPropNames(from))
17
+ if (!__hasOwnProp.call(to, key) && key !== except)
18
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
19
+ }
20
+ return to;
21
+ };
22
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
23
+ // If the importer is in node compatibility mode or this is not an ESM
24
+ // file that has been converted to a CommonJS file using a Babel-
25
+ // compatible transform (i.e. "__esModule" has not been set), then set
26
+ // "default" to the CommonJS "module.exports" for node compatibility.
27
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
28
+ mod
29
+ ));
30
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
31
+
32
+ // scripts/common.js
33
+ var require_common = __commonJS({
34
+ "scripts/common.js"(exports2, module2) {
35
+ var { globToRegexp } = require("storybook/internal/common");
36
+ var path2 = require("path");
37
+ var fs = require("fs");
38
+ var cwd2 = process.cwd();
39
+ var toRequireContext = (specifier) => {
40
+ const { directory, files } = specifier;
41
+ const match = globToRegexp(`./${files}`);
42
+ return {
43
+ path: directory,
44
+ recursive: files.includes("**") || files.split("/").length > 1,
45
+ match
46
+ };
47
+ };
48
+ var supportedExtensions = ["js", "jsx", "ts", "tsx", "cjs", "mjs"];
49
+ function getFilePathExtension({ configPath }, fileName) {
50
+ for (const ext of supportedExtensions) {
51
+ const filePath = path2.resolve(cwd2, configPath, `${fileName}.${ext}`);
52
+ if (fs.existsSync(filePath)) {
53
+ return ext;
54
+ }
55
+ }
56
+ return null;
57
+ }
58
+ function getFilePathWithExtension2({ configPath }, fileName) {
59
+ for (const ext of supportedExtensions) {
60
+ const filePath = path2.resolve(cwd2, configPath, `${fileName}.${ext}`);
61
+ if (fs.existsSync(filePath)) {
62
+ return filePath;
63
+ }
64
+ }
65
+ return null;
66
+ }
67
+ function ensureRelativePathHasDot2(relativePath) {
68
+ return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
69
+ }
70
+ function getPreviewExists({ configPath }) {
71
+ return !!getFilePathExtension({ configPath }, "preview");
72
+ }
73
+ function resolveAddonFile(addon, file, extensions = ["js", "mjs", "ts"], configPath) {
74
+ if (!addon || typeof addon !== "string") return null;
75
+ try {
76
+ const basePath = `${addon}/${file}`;
77
+ require.resolve(basePath);
78
+ return basePath;
79
+ } catch (_error) {
80
+ }
81
+ for (const ext of extensions) {
82
+ try {
83
+ const filePath = `${addon}/${file}.${ext}`;
84
+ require.resolve(filePath);
85
+ return filePath;
86
+ } catch (_error) {
87
+ }
88
+ }
89
+ if (addon.startsWith("./") || addon.startsWith("../")) {
90
+ try {
91
+ const extension = getFilePathExtension({ configPath }, `${addon}/${file}`);
92
+ if (extension) {
93
+ return `${addon}/${file}`;
94
+ }
95
+ } catch (_error) {
96
+ }
97
+ }
98
+ return null;
99
+ }
100
+ function getAddonName(addon) {
101
+ if (typeof addon === "string") return addon;
102
+ if (typeof addon === "object" && addon.name && typeof addon.name === "string") return addon.name;
103
+ console.error("Invalid addon configuration", addon);
104
+ return null;
105
+ }
106
+ module2.exports = {
107
+ toRequireContext,
108
+ getFilePathExtension,
109
+ ensureRelativePathHasDot: ensureRelativePathHasDot2,
110
+ getPreviewExists,
111
+ resolveAddonFile,
112
+ getAddonName,
113
+ getFilePathWithExtension: getFilePathWithExtension2
114
+ };
115
+ }
116
+ });
117
+
118
+ // src/node.ts
119
+ var node_exports = {};
120
+ __export(node_exports, {
121
+ buildIndex: () => buildIndex,
122
+ createChannelServer: () => createChannelServer
123
+ });
124
+ module.exports = __toCommonJS(node_exports);
125
+
126
+ // src/metro/channelServer.ts
127
+ var import_ws = require("ws");
128
+ var import_node_http = require("http");
129
+
130
+ // src/metro/buildIndex.ts
131
+ var import_common = require("storybook/internal/common");
132
+ var import_node_fs = require("fs");
133
+ var import_glob = require("glob");
134
+ var import_path = __toESM(require("path"));
135
+ var import_csf_tools = require("storybook/internal/csf-tools");
136
+ var import_csf = require("storybook/internal/csf");
137
+ var import_preview_api = require("storybook/internal/preview-api");
138
+ var import_common2 = __toESM(require_common());
139
+ var cwd = process.cwd();
140
+ var makeTitle = (fileName, specifier, userTitle) => {
141
+ const title = (0, import_preview_api.userOrAutoTitleFromSpecifier)(fileName, specifier, userTitle);
142
+ if (title) {
143
+ return title.replace("./", "");
144
+ } else if (userTitle) {
145
+ return userTitle.replace("./", "");
146
+ } else {
147
+ console.error("Could not generate title!!");
148
+ process.exit(1);
149
+ }
150
+ };
151
+ function ensureRelativePathHasDot(relativePath) {
152
+ return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
153
+ }
154
+ async function buildIndex({ configPath }) {
155
+ const main = await (0, import_common.loadMainConfig)({ configDir: configPath, cwd });
156
+ if (!main.stories || !Array.isArray(main.stories)) {
157
+ throw new Error("No stories found");
158
+ }
159
+ const storiesSpecifiers = (0, import_common.normalizeStories)(main.stories, {
160
+ configDir: configPath,
161
+ workingDir: cwd
162
+ });
163
+ const specifierStoryPaths = storiesSpecifiers.map((specifier) => {
164
+ return (0, import_glob.sync)(specifier.files, {
165
+ cwd: import_path.default.resolve(process.cwd(), specifier.directory),
166
+ absolute: true,
167
+ // default to always ignore (exclude) anything in node_modules
168
+ ignore: ["**/node_modules"]
169
+ }).map((storyPath) => {
170
+ const normalizePathForWindows = (str) => import_path.default.sep === "\\" ? str.replace(/\\/g, "/") : str;
171
+ return normalizePathForWindows(storyPath);
172
+ });
173
+ });
174
+ const csfStories = specifierStoryPaths.reduce(
175
+ (acc, specifierStoryPathList, specifierIndex) => {
176
+ const paths = specifierStoryPathList.map((storyPath) => {
177
+ const code = (0, import_node_fs.readFileSync)(storyPath, { encoding: "utf-8" }).toString();
178
+ const relativePath = ensureRelativePathHasDot(import_path.default.posix.relative(cwd, storyPath));
179
+ return {
180
+ result: (0, import_csf_tools.loadCsf)(code, {
181
+ fileName: storyPath,
182
+ makeTitle: (userTitle) => makeTitle(relativePath, storiesSpecifiers[specifierIndex], userTitle)
183
+ }).parse(),
184
+ specifier: storiesSpecifiers[specifierIndex],
185
+ fileName: relativePath
186
+ };
187
+ });
188
+ return [...acc, ...paths];
189
+ },
190
+ new Array()
191
+ );
192
+ const index = {
193
+ v: 5,
194
+ entries: {}
195
+ };
196
+ for (const { result, specifier, fileName } of csfStories) {
197
+ const { meta, stories } = result;
198
+ if (stories && stories.length > 0) {
199
+ for (const story of stories) {
200
+ const id = (0, import_csf.toId)(meta.title, story.name);
201
+ index.entries[id] = {
202
+ type: "story",
203
+ subtype: "story",
204
+ id,
205
+ name: story.name,
206
+ title: meta.title,
207
+ importPath: `${specifier.directory}/${import_path.default.posix.relative(specifier.directory, fileName)}`,
208
+ tags: ["story"]
209
+ };
210
+ }
211
+ } else {
212
+ console.log(`No stories found for ${fileName}`);
213
+ }
214
+ }
215
+ try {
216
+ const previewPath = (0, import_common2.getFilePathWithExtension)({ configPath }, "preview");
217
+ const previewSourceCode = (0, import_node_fs.readFileSync)(previewPath, { encoding: "utf-8" }).toString();
218
+ const storySort = (0, import_csf_tools.getStorySortParameter)(previewSourceCode);
219
+ const sortableStories = Object.values(index.entries);
220
+ (0, import_preview_api.sortStoriesV7)(
221
+ sortableStories,
222
+ storySort,
223
+ sortableStories.map((entry) => entry.importPath)
224
+ );
225
+ const sorted = sortableStories.reduce(
226
+ (acc, item) => {
227
+ acc[item.id] = item;
228
+ return acc;
229
+ },
230
+ {}
231
+ );
232
+ return { v: 5, entries: sorted };
233
+ } catch {
234
+ console.warn("Failed to sort stories, using unordered index");
235
+ return index;
236
+ }
237
+ }
238
+
239
+ // src/metro/channelServer.ts
240
+ function createChannelServer({
241
+ port = 7007,
242
+ host = void 0,
243
+ configPath
244
+ }) {
245
+ const httpServer = (0, import_node_http.createServer)(async (req, res) => {
246
+ if (req.method === "OPTIONS") {
247
+ res.writeHead(204);
248
+ res.end();
249
+ return;
250
+ }
251
+ if (req.method === "GET" && req.url === "/index.json") {
252
+ try {
253
+ const index = await buildIndex({ configPath });
254
+ res.writeHead(200, { "Content-Type": "application/json" });
255
+ res.end(JSON.stringify(index));
256
+ } catch (error) {
257
+ console.error("Failed to build index:", error);
258
+ res.writeHead(500, { "Content-Type": "application/json" });
259
+ res.end(JSON.stringify({ error: "Failed to build story index" }));
260
+ }
261
+ return;
262
+ }
263
+ if (req.method === "POST" && req.url === "/send-event") {
264
+ let body = "";
265
+ req.on("data", (chunk) => {
266
+ body += chunk.toString();
267
+ });
268
+ req.on("end", () => {
269
+ try {
270
+ const json = JSON.parse(body);
271
+ wss.clients.forEach((wsClient) => wsClient.send(JSON.stringify(json)));
272
+ res.writeHead(200, { "Content-Type": "application/json" });
273
+ res.end(JSON.stringify({ success: true }));
274
+ } catch (error) {
275
+ console.error("Failed to parse event:", error);
276
+ res.writeHead(400, { "Content-Type": "application/json" });
277
+ res.end(JSON.stringify({ success: false, error: "Invalid JSON" }));
278
+ }
279
+ });
280
+ return;
281
+ }
282
+ res.writeHead(404, { "Content-Type": "application/json" });
283
+ res.end(JSON.stringify({ error: "Not found" }));
284
+ });
285
+ const wss = new import_ws.WebSocketServer({ server: httpServer });
286
+ setInterval(function ping() {
287
+ wss.clients.forEach(function each(client) {
288
+ if (client.readyState === import_ws.WebSocket.OPEN) {
289
+ client.send(JSON.stringify({ type: "ping", args: [] }));
290
+ }
291
+ });
292
+ }, 1e4);
293
+ wss.on("connection", function connection(ws) {
294
+ console.log("WebSocket connection established");
295
+ ws.on("error", console.error);
296
+ ws.on("message", function message(data) {
297
+ try {
298
+ const json = JSON.parse(data.toString());
299
+ wss.clients.forEach((wsClient) => wsClient.send(JSON.stringify(json)));
300
+ } catch (error) {
301
+ console.error(error);
302
+ }
303
+ });
304
+ });
305
+ httpServer.listen(port, host, () => {
306
+ console.log(`WebSocket server listening on ${host ?? "localhost"}:${port}`);
307
+ });
308
+ return wss;
309
+ }
310
+ // Annotate the CommonJS export names for ESM import in node:
311
+ 0 && (module.exports = {
312
+ buildIndex,
313
+ createChannelServer
314
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@storybook/react-native",
3
- "version": "10.1.3",
3
+ "version": "10.2.0-beta.1",
4
4
  "description": "A better way to develop React Native Components for your app",
5
5
  "keywords": [
6
6
  "react",
@@ -24,6 +24,7 @@
24
24
  "exports": {
25
25
  ".": "./dist/index.js",
26
26
  "./metro/withStorybook": "./dist/metro/withStorybook.js",
27
+ "./node": "./dist/node.js",
27
28
  "./preview": "./dist/preview.js",
28
29
  "./scripts/generate": "./scripts/generate.js",
29
30
  "./preset": "./preset.js",
@@ -48,14 +49,15 @@
48
49
  "test:generate:update": "node --test --test-update-snapshots scripts/generate.test.js"
49
50
  },
50
51
  "dependencies": {
51
- "@storybook/react": "^10.1.3",
52
- "@storybook/react-native-theming": "^10.1.3",
53
- "@storybook/react-native-ui": "^10.1.3",
54
- "@storybook/react-native-ui-common": "^10.1.3",
52
+ "@storybook/react": "10.2.0-beta.1",
53
+ "@storybook/react-native-theming": "^10.2.0-beta.1",
54
+ "@storybook/react-native-ui": "^10.2.0-beta.1",
55
+ "@storybook/react-native-ui-common": "^10.2.0-beta.1",
55
56
  "commander": "^14.0.2",
56
57
  "dedent": "^1.7.0",
57
58
  "deepmerge": "^4.3.1",
58
59
  "esbuild-register": "^3.6.0",
60
+ "glob": "^13.0.0",
59
61
  "react-native-url-polyfill": "^3.0.0",
60
62
  "setimmediate": "^1.0.5",
61
63
  "ws": "^8.18.3"
@@ -70,7 +72,7 @@
70
72
  "jotai": "^2.6.2",
71
73
  "react": "19.1.0",
72
74
  "react-native": "0.81.5",
73
- "storybook": "^10.1.3",
75
+ "storybook": "10.2.0-beta.1",
74
76
  "tsup": "^8.5.0",
75
77
  "typescript": "~5.9.3",
76
78
  "universal-test-renderer": "^0.6.0"
@@ -104,5 +106,5 @@
104
106
  "publishConfig": {
105
107
  "access": "public"
106
108
  },
107
- "gitHead": "3a5146fd043a764842aa68d0e49172d9c3c14f78"
109
+ "gitHead": "decc26bd32be018bbc5ab0dc28bc5daff9342daa"
108
110
  }
package/readme.md CHANGED
@@ -11,7 +11,7 @@ If you are migrating from 9 to 10 you can find the migration guide [here](https:
11
11
 
12
12
  For more information about storybook visit: [storybook.js.org](https://storybook.js.org)
13
13
 
14
- > [!NOTE]
14
+ > [!NOTE]
15
15
  > Make sure you align your storybook dependencies to the same major version or you will see broken behaviour.
16
16
 
17
17
  ![picture of storybook](https://github.com/user-attachments/assets/cf98766d-8b90-44ab-b718-94ab16e63205)
@@ -34,14 +34,14 @@ For more information about storybook visit: [storybook.js.org](https://storybook
34
34
 
35
35
  There is some project boilerplate with `@storybook/react-native` and `@storybook/addon-react-native-web` both already configured with a simple example.
36
36
 
37
- For expo you can use this [template](https://github.com/dannyhw/expo-template-storybook) with the following command
37
+ For Expo you can use this [template](https://github.com/dannyhw/expo-template-storybook) with the following command
38
38
 
39
39
  ```sh
40
40
  # With NPM
41
41
  npx create-expo-app --template expo-template-storybook AwesomeStorybook
42
42
  ```
43
43
 
44
- For react native cli you can use this [template](https://github.com/dannyhw/react-native-template-storybook)
44
+ For React Native CLI you can use this [template](https://github.com/dannyhw/react-native-template-storybook)
45
45
 
46
46
  ```sh
47
47
  npx @react-native-community/cli init MyApp --template react-native-template-storybook
@@ -65,7 +65,7 @@ Then wrap your metro config with the withStorybook function as seen [below](#add
65
65
 
66
66
  If you want to be able to swap easily between storybook and your app, have a look at this [blog post](https://dev.to/dannyhw/how-to-swap-between-react-native-storybook-and-your-app-p3o)
67
67
 
68
- If you want to add everything yourself check out the the manual guide [here](https://github.com/storybookjs/react-native/blob/next/MANUAL_SETUP.md).
68
+ If you want to add everything yourself check out the manual guide [here](https://github.com/storybookjs/react-native/blob/next/MANUAL_SETUP.md).
69
69
 
70
70
  #### Additional steps: Update your metro config
71
71
 
@@ -107,7 +107,7 @@ module.exports = withStorybook(config, {
107
107
  });
108
108
  ```
109
109
 
110
- **React native**
110
+ **React Native**
111
111
 
112
112
  ```js
113
113
  const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
@@ -185,13 +185,13 @@ export { default } from '../.rnstorybook';
185
185
 
186
186
  Then add a way to navigate to your storybook route and I recommend disabling the header for the storybook route.
187
187
 
188
- Heres a video showing the same setup:
188
+ Here's a video showing the same setup:
189
189
 
190
190
  https://www.youtube.com/watch?v=egBqrYg0AIg
191
191
 
192
192
  ## Writing stories
193
193
 
194
- In storybook we use a syntax called CSF that looks like this:
194
+ In Storybook we use a syntax called CSF that looks like this:
195
195
 
196
196
  ```tsx
197
197
  import type { Meta, StoryObj } from '@storybook/react-native';
@@ -229,7 +229,7 @@ export default main;
229
229
 
230
230
  ### Decorators and Parameters
231
231
 
232
- For stories you can add decorators and parameters on the default export or on a specifc story.
232
+ For stories you can add decorators and parameters on the default export or on a specific story.
233
233
 
234
234
  ```tsx
235
235
  import type { Meta } from '@storybook/react';
@@ -263,7 +263,7 @@ For global decorators and parameters, you can add them to `preview.tsx` inside y
263
263
 
264
264
  ```tsx
265
265
  // .rnstorybook/preview.tsx
266
- import type {
266
+ import type { Preview } from '@storybook/react-native';
267
267
  import { withBackgrounds } from '@storybook/addon-ondevice-backgrounds';
268
268
 
269
269
  const preview: Preview = {
@@ -295,11 +295,11 @@ export default preview;
295
295
  The cli will install some basic addons for you such as controls and actions.
296
296
  Ondevice addons are addons that can render with the device ui that you see on the phone.
297
297
 
298
- Currently the addons available are:
298
+ Currently, the addons available are:
299
299
 
300
300
  - [`@storybook/addon-ondevice-controls`](https://storybook.js.org/addons/@storybook/addon-ondevice-controls): adjust your components props in realtime
301
301
  - [`@storybook/addon-ondevice-actions`](https://storybook.js.org/addons/@storybook/addon-ondevice-actions): mock onPress calls with actions that will log information in the actions tab
302
- - [`@storybook/addon-ondevice-notes`](https://storybook.js.org/addons/@storybook/addon-ondevice-notes): Add some markdown to your stories to help document their usage
302
+ - [`@storybook/addon-ondevice-notes`](https://storybook.js.org/addons/@storybook/addon-ondevice-notes): Add some Markdown to your stories to help document their usage
303
303
  - [`@storybook/addon-ondevice-backgrounds`](https://storybook.js.org/addons/@storybook/addon-ondevice-backgrounds): change the background of storybook to compare the look of your component against different backgrounds
304
304
 
305
305
  Install each one you want to use and add them to the `main.ts` addons list as follows:
@@ -465,29 +465,31 @@ The port on which to run the WebSocket, if specified.
465
465
 
466
466
  You can pass these parameters to getStorybookUI call in your storybook entry point:
467
467
 
468
- ```
468
+ ```ts
469
469
  {
470
- initialSelection?: string | Object (undefined)
471
- -- initialize storybook with a specific story. eg: `mybutton--largebutton` or `{ kind: 'MyButton', name: 'LargeButton' }`
472
- storage?: Object (undefined)
473
- -- {getItem: (key: string) => Promise<string | null>;setItem: (key: string, value: string) => Promise<void>;}
474
- -- Custom storage to be used instead of AsyncStorage
470
+ // initialize storybook with a specific story. eg: `mybutton--largebutton` or `{ kind: 'MyButton', name: 'LargeButton' }`
471
+ initialSelection?: string | Object;
472
+ // Custom storage to be used instead of AsyncStorage
473
+ storage?: {
474
+ getItem: (key: string) => Promise<string | null>;
475
+ setItem: (key: string, value: string) => Promise<void>;
476
+ };
477
+ // show the onDevice UI
475
478
  onDeviceUI?: boolean;
476
- -- show the ondevice ui
479
+ // enable websockets for the Storybook UI
477
480
  enableWebsockets?: boolean;
478
- -- enable websockets for the storybook ui
481
+ // query params for the websocket connection
479
482
  query?: string;
480
- -- query params for the websocket connection
483
+ // host for the websocket connection
481
484
  host?: string;
482
- -- host for the websocket connection
485
+ // port for the websocket connection
483
486
  port?: number;
484
- -- port for the websocket connection
487
+ // use secured websockets
485
488
  secured?: boolean;
486
- -- use secured websockets
489
+ // store the last selected story in the device's storage
487
490
  shouldPersistSelection?: boolean;
488
- -- store the last selected story in the device's storage
491
+ // theme for the Storybook UI
489
492
  theme: Partial<Theme>;
490
- -- theme for the storybook ui
491
493
  }
492
494
  ```
493
495
 
@@ -500,7 +502,7 @@ Storybook provides testing utilities that allow you to reuse your stories in ext
500
502
  We welcome contributions to Storybook!
501
503
 
502
504
  - 📥 Pull requests and 🌟 Stars are always welcome.
503
- - Read our [contributing guide](CONTRIBUTING.md) to get started,
505
+ - Read our [contributing guide](../../CONTRIBUTING.md) to get started,
504
506
  or find us on [Discord](https://discord.gg/sMFvFsG) and look for the react-native channel.
505
507
 
506
508
  Looking for a first issue to tackle?
@@ -514,6 +516,6 @@ Here are some example projects to help you get started
514
516
 
515
517
  - A mono repo setup by @axeldelafosse https://github.com/axeldelafosse/storybook-rnw-monorepo
516
518
  - Expo setup https://github.com/dannyhw/expo-storybook-starter
517
- - React native cli setup https://github.com/dannyhw/react-native-storybook-starter
519
+ - React Native CLI setup https://github.com/dannyhw/react-native-storybook-starter
518
520
  - Adding a separate entry point and dev menu item in native files for RN CLI project: https://github.com/zubko/react-native-storybook-with-dev-menu
519
521
  - Want to showcase your own project? open a PR and add it to the list!
package/scripts/common.js CHANGED
@@ -32,6 +32,18 @@ function getFilePathExtension({ configPath }, fileName) {
32
32
  return null;
33
33
  }
34
34
 
35
+ function getFilePathWithExtension({ configPath }, fileName) {
36
+ for (const ext of supportedExtensions) {
37
+ const filePath = path.resolve(cwd, configPath, `${fileName}.${ext}`);
38
+
39
+ if (fs.existsSync(filePath)) {
40
+ return filePath;
41
+ }
42
+ }
43
+
44
+ return null;
45
+ }
46
+
35
47
  function ensureRelativePathHasDot(relativePath) {
36
48
  return relativePath.startsWith('.') ? relativePath : `./${relativePath}`;
37
49
  }
@@ -92,4 +104,5 @@ module.exports = {
92
104
  getPreviewExists,
93
105
  resolveAddonFile,
94
106
  getAddonName,
107
+ getFilePathWithExtension,
95
108
  };
@@ -8,6 +8,7 @@ const {
8
8
  const { normalizeStories, globToRegexp, loadMainConfig } = require('storybook/internal/common');
9
9
  const { interopRequireDefault } = require('./require-interop');
10
10
  const fs = require('fs');
11
+ const { networkInterfaces } = require('node:os');
11
12
 
12
13
  const path = require('path');
13
14
 
@@ -32,7 +33,32 @@ const loadMain = async ({ configPath, cwd }) => {
32
33
  }
33
34
  };
34
35
 
35
- async function generate({ configPath, /* absolute = false, */ useJs = false, docTools = true }) {
36
+ /**
37
+ * Get the local IP address of the machine.
38
+ * @returns The local IP address of the machine.
39
+ */
40
+ function getLocalIPAddress() {
41
+ const nets = networkInterfaces();
42
+ for (const name of Object.keys(nets)) {
43
+ for (const net of nets[name]) {
44
+ const familyV4Value = typeof net.family === 'string' ? 'IPv4' : 4;
45
+ if (net.family === familyV4Value && !net.internal) {
46
+ return net.address;
47
+ }
48
+ }
49
+ }
50
+ return '0.0.0.0';
51
+ }
52
+
53
+ async function generate({
54
+ configPath,
55
+ useJs = false,
56
+ docTools = true,
57
+ host = undefined,
58
+ port = 7007,
59
+ }) {
60
+ // here we want to get the ip address and pass it to rn storybook so that devices can connect over lan easily
61
+ const channelHost = host === 'auto' ? getLocalIPAddress() : host;
36
62
  const storybookRequiresLocation = path.resolve(
37
63
  cwd,
38
64
  configPath,
@@ -127,6 +153,7 @@ async function generate({ configPath, /* absolute = false, */ useJs = false, doc
127
153
  declare global {
128
154
  var view: View;
129
155
  var STORIES: typeof normalizedStories;
156
+ var STORYBOOK_WEBSOCKET: { host: string; port: number } | undefined;
130
157
  }
131
158
  `;
132
159
 
@@ -143,24 +170,25 @@ ${useJs ? '' : globalTypes}
143
170
 
144
171
  const annotations = ${annotations};
145
172
 
146
- global.STORIES = normalizedStories;
173
+ globalThis.STORIES = normalizedStories;
174
+ ${channelHost ? `globalThis.STORYBOOK_WEBSOCKET = { host: '${channelHost}', port: ${port ?? 7007} };` : ''}
147
175
 
148
176
  ${useJs ? '' : '// @ts-ignore'}
149
177
  module?.hot?.accept?.();
150
178
 
151
179
  ${optionsVar}
152
180
 
153
- if (!global.view) {
154
- global.view = start({
181
+ if (!globalThis.view) {
182
+ globalThis.view = start({
155
183
  annotations,
156
184
  storyEntries: normalizedStories,
157
185
  ${options ? ` ${options},` : ''}
158
186
  });
159
187
  } else {
160
- updateView(global.view, annotations, normalizedStories${options ? `, ${options}` : ''});
188
+ updateView(globalThis.view, annotations, normalizedStories${options ? `, ${options}` : ''});
161
189
  }
162
190
 
163
- export const view${useJs ? '' : ': View'} = global.view;
191
+ export const view${useJs ? '' : ': View'} = globalThis.view;
164
192
  `;
165
193
 
166
194
  fs.writeFileSync(storybookRequiresLocation, fileContent, {