@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/index.d.ts +10 -1
- package/dist/index.js +40 -51
- package/dist/metro/withStorybook.d.ts +1 -1
- package/dist/metro/withStorybook.js +261 -52
- package/dist/node.d.ts +40 -0
- package/dist/node.js +314 -0
- package/package.json +9 -7
- package/readme.md +29 -27
- package/scripts/common.js +13 -0
- package/scripts/generate.js +34 -6
- package/scripts/generate.test.js +82 -0
- package/scripts/generate.test.js.snapshot +25 -5
- package/scripts/handle-args.js +11 -1
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
|
+
"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": "
|
|
52
|
-
"@storybook/react-native-theming": "^10.1
|
|
53
|
-
"@storybook/react-native-ui": "^10.1
|
|
54
|
-
"@storybook/react-native-ui-common": "^10.1
|
|
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": "
|
|
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": "
|
|
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
|

|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
471
|
-
|
|
472
|
-
storage
|
|
473
|
-
|
|
474
|
-
|
|
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
|
-
|
|
479
|
+
// enable websockets for the Storybook UI
|
|
477
480
|
enableWebsockets?: boolean;
|
|
478
|
-
|
|
481
|
+
// query params for the websocket connection
|
|
479
482
|
query?: string;
|
|
480
|
-
|
|
483
|
+
// host for the websocket connection
|
|
481
484
|
host?: string;
|
|
482
|
-
|
|
485
|
+
// port for the websocket connection
|
|
483
486
|
port?: number;
|
|
484
|
-
|
|
487
|
+
// use secured websockets
|
|
485
488
|
secured?: boolean;
|
|
486
|
-
|
|
489
|
+
// store the last selected story in the device's storage
|
|
487
490
|
shouldPersistSelection?: boolean;
|
|
488
|
-
|
|
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
|
|
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
|
};
|
package/scripts/generate.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 (!
|
|
154
|
-
|
|
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(
|
|
188
|
+
updateView(globalThis.view, annotations, normalizedStories${options ? `, ${options}` : ''});
|
|
161
189
|
}
|
|
162
190
|
|
|
163
|
-
export const view${useJs ? '' : ': View'} =
|
|
191
|
+
export const view${useJs ? '' : ': View'} = globalThis.view;
|
|
164
192
|
`;
|
|
165
193
|
|
|
166
194
|
fs.writeFileSync(storybookRequiresLocation, fileContent, {
|