@storybook/react-native 10.3.0-next.4 → 10.3.0-next.5
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/metro/withStorybook.js +190 -9
- package/dist/node.d.ts +1 -0
- package/dist/node.js +190 -9
- package/dist/repack/withStorybook.js +190 -9
- package/package.json +6 -6
- package/readme.md +1 -0
|
@@ -391,7 +391,10 @@ async function buildIndex({ configPath }) {
|
|
|
391
391
|
const { meta, stories } = result;
|
|
392
392
|
if (stories && stories.length > 0) {
|
|
393
393
|
for (const story of stories) {
|
|
394
|
-
const id = (0, import_csf.toId)(meta.title, story.name);
|
|
394
|
+
const id = story.id ?? (0, import_csf.toId)(meta.title, story.name);
|
|
395
|
+
if (!id) {
|
|
396
|
+
throw new Error(`Failed to generate id for story ${story.name} in file ${fileName}`);
|
|
397
|
+
}
|
|
395
398
|
index.entries[id] = {
|
|
396
399
|
type: "story",
|
|
397
400
|
subtype: "story",
|
|
@@ -569,7 +572,7 @@ var import_common3 = require("storybook/internal/common");
|
|
|
569
572
|
var import_telemetry = require("storybook/internal/telemetry");
|
|
570
573
|
|
|
571
574
|
// src/metro/channelServer.ts
|
|
572
|
-
var
|
|
575
|
+
var import_ws2 = require("ws");
|
|
573
576
|
var import_node_http = require("http");
|
|
574
577
|
init_buildIndex();
|
|
575
578
|
|
|
@@ -780,6 +783,168 @@ function createMcpHandler(configPath, wss) {
|
|
|
780
783
|
return { handleMcpRequest, preInit };
|
|
781
784
|
}
|
|
782
785
|
|
|
786
|
+
// src/metro/selectStorySyncEndpoint.ts
|
|
787
|
+
var import_ws = require("ws");
|
|
788
|
+
var SELECT_STORY_SYNC_ROUTE = "/select-story-sync/";
|
|
789
|
+
var SELECT_STORY_SYNC_TIMEOUT_MS = 1e3;
|
|
790
|
+
var LAST_RENDERED_STORY_TIMEOUT_MS = 500;
|
|
791
|
+
function getRenderedStoryId(event) {
|
|
792
|
+
if (!event || typeof event !== "object") {
|
|
793
|
+
return null;
|
|
794
|
+
}
|
|
795
|
+
const { type, args } = event;
|
|
796
|
+
if (type !== "storyRendered" || !Array.isArray(args) || args.length === 0) {
|
|
797
|
+
return null;
|
|
798
|
+
}
|
|
799
|
+
const [firstArg] = args;
|
|
800
|
+
if (typeof firstArg === "string") {
|
|
801
|
+
return firstArg;
|
|
802
|
+
}
|
|
803
|
+
if (firstArg && typeof firstArg === "object" && "storyId" in firstArg) {
|
|
804
|
+
const { storyId } = firstArg;
|
|
805
|
+
return typeof storyId === "string" ? storyId : null;
|
|
806
|
+
}
|
|
807
|
+
return null;
|
|
808
|
+
}
|
|
809
|
+
function parseStoryIdFromPath(pathname) {
|
|
810
|
+
const match = pathname.match(/^\/select-story-sync\/([^/]+)$/);
|
|
811
|
+
if (!match) {
|
|
812
|
+
return null;
|
|
813
|
+
}
|
|
814
|
+
try {
|
|
815
|
+
const storyId = decodeURIComponent(match[1]);
|
|
816
|
+
return storyId || null;
|
|
817
|
+
} catch {
|
|
818
|
+
return null;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
function createSelectStorySyncEndpoint(wss) {
|
|
822
|
+
const pendingStorySelections = /* @__PURE__ */ new Map();
|
|
823
|
+
const lastRenderedStoryIdByClient = /* @__PURE__ */ new Map();
|
|
824
|
+
const waitForStoryRender = (storyId, timeoutMs) => {
|
|
825
|
+
let cancelSelection = () => {
|
|
826
|
+
};
|
|
827
|
+
let resolveWait = () => {
|
|
828
|
+
};
|
|
829
|
+
const promise = new Promise((resolve2, reject) => {
|
|
830
|
+
resolveWait = resolve2;
|
|
831
|
+
let selections = pendingStorySelections.get(storyId);
|
|
832
|
+
if (!selections) {
|
|
833
|
+
selections = /* @__PURE__ */ new Set();
|
|
834
|
+
pendingStorySelections.set(storyId, selections);
|
|
835
|
+
}
|
|
836
|
+
const cleanup = () => {
|
|
837
|
+
clearTimeout(selection.timeout);
|
|
838
|
+
selections.delete(selection);
|
|
839
|
+
if (selections.size === 0) {
|
|
840
|
+
pendingStorySelections.delete(storyId);
|
|
841
|
+
}
|
|
842
|
+
};
|
|
843
|
+
const selection = {
|
|
844
|
+
resolve: () => {
|
|
845
|
+
if (selection.settled) {
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
selection.settled = true;
|
|
849
|
+
cleanup();
|
|
850
|
+
resolve2();
|
|
851
|
+
},
|
|
852
|
+
timeout: setTimeout(() => {
|
|
853
|
+
if (selection.settled) {
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
selection.settled = true;
|
|
857
|
+
cleanup();
|
|
858
|
+
reject(new Error(`Story "${storyId}" did not render in time`));
|
|
859
|
+
}, timeoutMs),
|
|
860
|
+
settled: false
|
|
861
|
+
};
|
|
862
|
+
cancelSelection = () => {
|
|
863
|
+
if (selection.settled) {
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
selection.settled = true;
|
|
867
|
+
cleanup();
|
|
868
|
+
resolveWait();
|
|
869
|
+
};
|
|
870
|
+
selections.add(selection);
|
|
871
|
+
});
|
|
872
|
+
return {
|
|
873
|
+
promise,
|
|
874
|
+
cancel: cancelSelection
|
|
875
|
+
};
|
|
876
|
+
};
|
|
877
|
+
const resolveStorySelection = (storyId) => {
|
|
878
|
+
const selections = pendingStorySelections.get(storyId);
|
|
879
|
+
if (!selections) {
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
[...selections].forEach((selection) => selection.resolve());
|
|
883
|
+
};
|
|
884
|
+
const handleRequest = async (pathname, res) => {
|
|
885
|
+
const storyId = parseStoryIdFromPath(pathname);
|
|
886
|
+
if (!storyId) {
|
|
887
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
888
|
+
res.end(JSON.stringify({ success: false, error: "Invalid story id" }));
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
const waitForRender = waitForStoryRender(storyId, SELECT_STORY_SYNC_TIMEOUT_MS);
|
|
892
|
+
const message = JSON.stringify({
|
|
893
|
+
type: "setCurrentStory",
|
|
894
|
+
args: [{ viewMode: "story", storyId }]
|
|
895
|
+
});
|
|
896
|
+
wss.clients.forEach((wsClient) => {
|
|
897
|
+
if (wsClient.readyState === import_ws.WebSocket.OPEN) {
|
|
898
|
+
wsClient.send(message);
|
|
899
|
+
}
|
|
900
|
+
});
|
|
901
|
+
try {
|
|
902
|
+
const hasConnectedClientWithRenderedStory = [...wss.clients].some(
|
|
903
|
+
(client) => client.readyState === import_ws.WebSocket.OPEN && lastRenderedStoryIdByClient.get(client) === storyId
|
|
904
|
+
);
|
|
905
|
+
if (hasConnectedClientWithRenderedStory) {
|
|
906
|
+
const raceResult = await Promise.race([
|
|
907
|
+
waitForRender.promise.then(() => "rendered"),
|
|
908
|
+
new Promise((resolve2) => {
|
|
909
|
+
setTimeout(() => resolve2("alreadyRendered"), LAST_RENDERED_STORY_TIMEOUT_MS);
|
|
910
|
+
})
|
|
911
|
+
]);
|
|
912
|
+
if (raceResult === "alreadyRendered") {
|
|
913
|
+
waitForRender.cancel();
|
|
914
|
+
}
|
|
915
|
+
} else {
|
|
916
|
+
await waitForRender.promise;
|
|
917
|
+
}
|
|
918
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
919
|
+
res.end(JSON.stringify({ success: true, storyId }));
|
|
920
|
+
} catch (error) {
|
|
921
|
+
res.writeHead(408, { "Content-Type": "application/json" });
|
|
922
|
+
res.end(
|
|
923
|
+
JSON.stringify({
|
|
924
|
+
success: false,
|
|
925
|
+
storyId,
|
|
926
|
+
error: error instanceof Error ? error.message : String(error)
|
|
927
|
+
})
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
};
|
|
931
|
+
const onSocketMessage = (event, ws) => {
|
|
932
|
+
const renderedStoryId = getRenderedStoryId(event);
|
|
933
|
+
if (renderedStoryId) {
|
|
934
|
+
lastRenderedStoryIdByClient.set(ws, renderedStoryId);
|
|
935
|
+
resolveStorySelection(renderedStoryId);
|
|
936
|
+
}
|
|
937
|
+
};
|
|
938
|
+
const onSocketClose = (ws) => {
|
|
939
|
+
lastRenderedStoryIdByClient.delete(ws);
|
|
940
|
+
};
|
|
941
|
+
return {
|
|
942
|
+
handleRequest,
|
|
943
|
+
onSocketMessage,
|
|
944
|
+
onSocketClose
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
|
|
783
948
|
// src/metro/channelServer.ts
|
|
784
949
|
function createChannelServer({
|
|
785
950
|
port = 7007,
|
|
@@ -789,15 +954,17 @@ function createChannelServer({
|
|
|
789
954
|
websockets = true
|
|
790
955
|
}) {
|
|
791
956
|
const httpServer = (0, import_node_http.createServer)();
|
|
792
|
-
const wss = websockets ? new
|
|
957
|
+
const wss = websockets ? new import_ws2.WebSocketServer({ server: httpServer }) : null;
|
|
793
958
|
const mcpServer = experimental_mcp ? createMcpHandler(configPath, wss ?? void 0) : null;
|
|
959
|
+
const selectStorySyncEndpoint = wss ? createSelectStorySyncEndpoint(wss) : null;
|
|
794
960
|
httpServer.on("request", async (req, res) => {
|
|
961
|
+
const requestUrl = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
795
962
|
if (req.method === "OPTIONS") {
|
|
796
963
|
res.writeHead(204);
|
|
797
964
|
res.end();
|
|
798
965
|
return;
|
|
799
966
|
}
|
|
800
|
-
if (req.method === "GET" &&
|
|
967
|
+
if (req.method === "GET" && requestUrl.pathname === "/index.json") {
|
|
801
968
|
try {
|
|
802
969
|
const index = await buildIndex({ configPath });
|
|
803
970
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
@@ -809,7 +976,7 @@ function createChannelServer({
|
|
|
809
976
|
}
|
|
810
977
|
return;
|
|
811
978
|
}
|
|
812
|
-
if (req.method === "POST" &&
|
|
979
|
+
if (req.method === "POST" && requestUrl.pathname === "/send-event") {
|
|
813
980
|
if (!wss) {
|
|
814
981
|
res.writeHead(503, { "Content-Type": "application/json" });
|
|
815
982
|
res.end(JSON.stringify({ success: false, error: "WebSockets are disabled" }));
|
|
@@ -833,7 +1000,16 @@ function createChannelServer({
|
|
|
833
1000
|
});
|
|
834
1001
|
return;
|
|
835
1002
|
}
|
|
836
|
-
if (
|
|
1003
|
+
if (req.method === "POST" && requestUrl.pathname.startsWith(SELECT_STORY_SYNC_ROUTE)) {
|
|
1004
|
+
if (!selectStorySyncEndpoint) {
|
|
1005
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
1006
|
+
res.end(JSON.stringify({ success: false, error: "WebSockets are disabled" }));
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
await selectStorySyncEndpoint.handleRequest(requestUrl.pathname, res);
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
if (mcpServer && requestUrl.pathname === "/mcp" && (req.method === "POST" || req.method === "GET")) {
|
|
837
1013
|
await mcpServer.handleMcpRequest(req, res);
|
|
838
1014
|
return;
|
|
839
1015
|
}
|
|
@@ -843,22 +1019,24 @@ function createChannelServer({
|
|
|
843
1019
|
if (wss) {
|
|
844
1020
|
wss.on("error", () => {
|
|
845
1021
|
});
|
|
846
|
-
setInterval(function ping() {
|
|
1022
|
+
const pingInterval = setInterval(function ping() {
|
|
847
1023
|
wss.clients.forEach(function each(client) {
|
|
848
|
-
if (client.readyState ===
|
|
1024
|
+
if (client.readyState === import_ws2.WebSocket.OPEN) {
|
|
849
1025
|
client.send(JSON.stringify({ type: "ping", args: [] }));
|
|
850
1026
|
}
|
|
851
1027
|
});
|
|
852
1028
|
}, 1e4);
|
|
1029
|
+
pingInterval.unref?.();
|
|
853
1030
|
wss.on("connection", function connection(ws) {
|
|
854
1031
|
console.log("WebSocket connection established");
|
|
855
1032
|
ws.on("error", console.error);
|
|
856
1033
|
ws.on("message", function message(data) {
|
|
857
1034
|
try {
|
|
858
1035
|
const json = JSON.parse(data.toString());
|
|
1036
|
+
selectStorySyncEndpoint?.onSocketMessage(json, ws);
|
|
859
1037
|
const msg = JSON.stringify(json);
|
|
860
1038
|
wss.clients.forEach((wsClient) => {
|
|
861
|
-
if (wsClient !== ws && wsClient.readyState ===
|
|
1039
|
+
if (wsClient !== ws && wsClient.readyState === import_ws2.WebSocket.OPEN) {
|
|
862
1040
|
wsClient.send(msg);
|
|
863
1041
|
}
|
|
864
1042
|
});
|
|
@@ -866,6 +1044,9 @@ function createChannelServer({
|
|
|
866
1044
|
console.error(error);
|
|
867
1045
|
}
|
|
868
1046
|
});
|
|
1047
|
+
ws.on("close", () => {
|
|
1048
|
+
selectStorySyncEndpoint?.onSocketClose(ws);
|
|
1049
|
+
});
|
|
869
1050
|
});
|
|
870
1051
|
}
|
|
871
1052
|
httpServer.on("error", (error) => {
|
package/dist/node.d.ts
CHANGED
|
@@ -33,6 +33,7 @@ interface ChannelServerOptions {
|
|
|
33
33
|
* The server provides both WebSocket and REST endpoints:
|
|
34
34
|
* - WebSocket: broadcasts all received messages to all connected clients
|
|
35
35
|
* - POST /send-event: sends an event to all WebSocket clients
|
|
36
|
+
* - POST /select-story-sync/{storyId}: sets the current story and waits for a storyRendered event
|
|
36
37
|
* - GET /index.json: returns the story index built from story files
|
|
37
38
|
* - POST /mcp: MCP endpoint for AI agent integration (when experimental_mcp option is enabled)
|
|
38
39
|
*
|
package/dist/node.js
CHANGED
|
@@ -173,7 +173,10 @@ async function buildIndex({ configPath }) {
|
|
|
173
173
|
const { meta, stories } = result;
|
|
174
174
|
if (stories && stories.length > 0) {
|
|
175
175
|
for (const story of stories) {
|
|
176
|
-
const id = (0, import_csf.toId)(meta.title, story.name);
|
|
176
|
+
const id = story.id ?? (0, import_csf.toId)(meta.title, story.name);
|
|
177
|
+
if (!id) {
|
|
178
|
+
throw new Error(`Failed to generate id for story ${story.name} in file ${fileName}`);
|
|
179
|
+
}
|
|
177
180
|
index.entries[id] = {
|
|
178
181
|
type: "story",
|
|
179
182
|
subtype: "story",
|
|
@@ -348,7 +351,7 @@ __export(node_exports, {
|
|
|
348
351
|
module.exports = __toCommonJS(node_exports);
|
|
349
352
|
|
|
350
353
|
// src/metro/channelServer.ts
|
|
351
|
-
var
|
|
354
|
+
var import_ws2 = require("ws");
|
|
352
355
|
var import_node_http = require("http");
|
|
353
356
|
init_buildIndex();
|
|
354
357
|
|
|
@@ -559,6 +562,168 @@ function createMcpHandler(configPath, wss) {
|
|
|
559
562
|
return { handleMcpRequest, preInit };
|
|
560
563
|
}
|
|
561
564
|
|
|
565
|
+
// src/metro/selectStorySyncEndpoint.ts
|
|
566
|
+
var import_ws = require("ws");
|
|
567
|
+
var SELECT_STORY_SYNC_ROUTE = "/select-story-sync/";
|
|
568
|
+
var SELECT_STORY_SYNC_TIMEOUT_MS = 1e3;
|
|
569
|
+
var LAST_RENDERED_STORY_TIMEOUT_MS = 500;
|
|
570
|
+
function getRenderedStoryId(event) {
|
|
571
|
+
if (!event || typeof event !== "object") {
|
|
572
|
+
return null;
|
|
573
|
+
}
|
|
574
|
+
const { type, args } = event;
|
|
575
|
+
if (type !== "storyRendered" || !Array.isArray(args) || args.length === 0) {
|
|
576
|
+
return null;
|
|
577
|
+
}
|
|
578
|
+
const [firstArg] = args;
|
|
579
|
+
if (typeof firstArg === "string") {
|
|
580
|
+
return firstArg;
|
|
581
|
+
}
|
|
582
|
+
if (firstArg && typeof firstArg === "object" && "storyId" in firstArg) {
|
|
583
|
+
const { storyId } = firstArg;
|
|
584
|
+
return typeof storyId === "string" ? storyId : null;
|
|
585
|
+
}
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
function parseStoryIdFromPath(pathname) {
|
|
589
|
+
const match = pathname.match(/^\/select-story-sync\/([^/]+)$/);
|
|
590
|
+
if (!match) {
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
try {
|
|
594
|
+
const storyId = decodeURIComponent(match[1]);
|
|
595
|
+
return storyId || null;
|
|
596
|
+
} catch {
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
function createSelectStorySyncEndpoint(wss) {
|
|
601
|
+
const pendingStorySelections = /* @__PURE__ */ new Map();
|
|
602
|
+
const lastRenderedStoryIdByClient = /* @__PURE__ */ new Map();
|
|
603
|
+
const waitForStoryRender = (storyId, timeoutMs) => {
|
|
604
|
+
let cancelSelection = () => {
|
|
605
|
+
};
|
|
606
|
+
let resolveWait = () => {
|
|
607
|
+
};
|
|
608
|
+
const promise = new Promise((resolve, reject) => {
|
|
609
|
+
resolveWait = resolve;
|
|
610
|
+
let selections = pendingStorySelections.get(storyId);
|
|
611
|
+
if (!selections) {
|
|
612
|
+
selections = /* @__PURE__ */ new Set();
|
|
613
|
+
pendingStorySelections.set(storyId, selections);
|
|
614
|
+
}
|
|
615
|
+
const cleanup = () => {
|
|
616
|
+
clearTimeout(selection.timeout);
|
|
617
|
+
selections.delete(selection);
|
|
618
|
+
if (selections.size === 0) {
|
|
619
|
+
pendingStorySelections.delete(storyId);
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
const selection = {
|
|
623
|
+
resolve: () => {
|
|
624
|
+
if (selection.settled) {
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
selection.settled = true;
|
|
628
|
+
cleanup();
|
|
629
|
+
resolve();
|
|
630
|
+
},
|
|
631
|
+
timeout: setTimeout(() => {
|
|
632
|
+
if (selection.settled) {
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
selection.settled = true;
|
|
636
|
+
cleanup();
|
|
637
|
+
reject(new Error(`Story "${storyId}" did not render in time`));
|
|
638
|
+
}, timeoutMs),
|
|
639
|
+
settled: false
|
|
640
|
+
};
|
|
641
|
+
cancelSelection = () => {
|
|
642
|
+
if (selection.settled) {
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
selection.settled = true;
|
|
646
|
+
cleanup();
|
|
647
|
+
resolveWait();
|
|
648
|
+
};
|
|
649
|
+
selections.add(selection);
|
|
650
|
+
});
|
|
651
|
+
return {
|
|
652
|
+
promise,
|
|
653
|
+
cancel: cancelSelection
|
|
654
|
+
};
|
|
655
|
+
};
|
|
656
|
+
const resolveStorySelection = (storyId) => {
|
|
657
|
+
const selections = pendingStorySelections.get(storyId);
|
|
658
|
+
if (!selections) {
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
[...selections].forEach((selection) => selection.resolve());
|
|
662
|
+
};
|
|
663
|
+
const handleRequest = async (pathname, res) => {
|
|
664
|
+
const storyId = parseStoryIdFromPath(pathname);
|
|
665
|
+
if (!storyId) {
|
|
666
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
667
|
+
res.end(JSON.stringify({ success: false, error: "Invalid story id" }));
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
const waitForRender = waitForStoryRender(storyId, SELECT_STORY_SYNC_TIMEOUT_MS);
|
|
671
|
+
const message = JSON.stringify({
|
|
672
|
+
type: "setCurrentStory",
|
|
673
|
+
args: [{ viewMode: "story", storyId }]
|
|
674
|
+
});
|
|
675
|
+
wss.clients.forEach((wsClient) => {
|
|
676
|
+
if (wsClient.readyState === import_ws.WebSocket.OPEN) {
|
|
677
|
+
wsClient.send(message);
|
|
678
|
+
}
|
|
679
|
+
});
|
|
680
|
+
try {
|
|
681
|
+
const hasConnectedClientWithRenderedStory = [...wss.clients].some(
|
|
682
|
+
(client) => client.readyState === import_ws.WebSocket.OPEN && lastRenderedStoryIdByClient.get(client) === storyId
|
|
683
|
+
);
|
|
684
|
+
if (hasConnectedClientWithRenderedStory) {
|
|
685
|
+
const raceResult = await Promise.race([
|
|
686
|
+
waitForRender.promise.then(() => "rendered"),
|
|
687
|
+
new Promise((resolve) => {
|
|
688
|
+
setTimeout(() => resolve("alreadyRendered"), LAST_RENDERED_STORY_TIMEOUT_MS);
|
|
689
|
+
})
|
|
690
|
+
]);
|
|
691
|
+
if (raceResult === "alreadyRendered") {
|
|
692
|
+
waitForRender.cancel();
|
|
693
|
+
}
|
|
694
|
+
} else {
|
|
695
|
+
await waitForRender.promise;
|
|
696
|
+
}
|
|
697
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
698
|
+
res.end(JSON.stringify({ success: true, storyId }));
|
|
699
|
+
} catch (error) {
|
|
700
|
+
res.writeHead(408, { "Content-Type": "application/json" });
|
|
701
|
+
res.end(
|
|
702
|
+
JSON.stringify({
|
|
703
|
+
success: false,
|
|
704
|
+
storyId,
|
|
705
|
+
error: error instanceof Error ? error.message : String(error)
|
|
706
|
+
})
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
};
|
|
710
|
+
const onSocketMessage = (event, ws) => {
|
|
711
|
+
const renderedStoryId = getRenderedStoryId(event);
|
|
712
|
+
if (renderedStoryId) {
|
|
713
|
+
lastRenderedStoryIdByClient.set(ws, renderedStoryId);
|
|
714
|
+
resolveStorySelection(renderedStoryId);
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
const onSocketClose = (ws) => {
|
|
718
|
+
lastRenderedStoryIdByClient.delete(ws);
|
|
719
|
+
};
|
|
720
|
+
return {
|
|
721
|
+
handleRequest,
|
|
722
|
+
onSocketMessage,
|
|
723
|
+
onSocketClose
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
|
|
562
727
|
// src/metro/channelServer.ts
|
|
563
728
|
function createChannelServer({
|
|
564
729
|
port = 7007,
|
|
@@ -568,15 +733,17 @@ function createChannelServer({
|
|
|
568
733
|
websockets = true
|
|
569
734
|
}) {
|
|
570
735
|
const httpServer = (0, import_node_http.createServer)();
|
|
571
|
-
const wss = websockets ? new
|
|
736
|
+
const wss = websockets ? new import_ws2.WebSocketServer({ server: httpServer }) : null;
|
|
572
737
|
const mcpServer = experimental_mcp ? createMcpHandler(configPath, wss ?? void 0) : null;
|
|
738
|
+
const selectStorySyncEndpoint = wss ? createSelectStorySyncEndpoint(wss) : null;
|
|
573
739
|
httpServer.on("request", async (req, res) => {
|
|
740
|
+
const requestUrl = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
574
741
|
if (req.method === "OPTIONS") {
|
|
575
742
|
res.writeHead(204);
|
|
576
743
|
res.end();
|
|
577
744
|
return;
|
|
578
745
|
}
|
|
579
|
-
if (req.method === "GET" &&
|
|
746
|
+
if (req.method === "GET" && requestUrl.pathname === "/index.json") {
|
|
580
747
|
try {
|
|
581
748
|
const index = await buildIndex({ configPath });
|
|
582
749
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
@@ -588,7 +755,7 @@ function createChannelServer({
|
|
|
588
755
|
}
|
|
589
756
|
return;
|
|
590
757
|
}
|
|
591
|
-
if (req.method === "POST" &&
|
|
758
|
+
if (req.method === "POST" && requestUrl.pathname === "/send-event") {
|
|
592
759
|
if (!wss) {
|
|
593
760
|
res.writeHead(503, { "Content-Type": "application/json" });
|
|
594
761
|
res.end(JSON.stringify({ success: false, error: "WebSockets are disabled" }));
|
|
@@ -612,7 +779,16 @@ function createChannelServer({
|
|
|
612
779
|
});
|
|
613
780
|
return;
|
|
614
781
|
}
|
|
615
|
-
if (
|
|
782
|
+
if (req.method === "POST" && requestUrl.pathname.startsWith(SELECT_STORY_SYNC_ROUTE)) {
|
|
783
|
+
if (!selectStorySyncEndpoint) {
|
|
784
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
785
|
+
res.end(JSON.stringify({ success: false, error: "WebSockets are disabled" }));
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
await selectStorySyncEndpoint.handleRequest(requestUrl.pathname, res);
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
if (mcpServer && requestUrl.pathname === "/mcp" && (req.method === "POST" || req.method === "GET")) {
|
|
616
792
|
await mcpServer.handleMcpRequest(req, res);
|
|
617
793
|
return;
|
|
618
794
|
}
|
|
@@ -622,22 +798,24 @@ function createChannelServer({
|
|
|
622
798
|
if (wss) {
|
|
623
799
|
wss.on("error", () => {
|
|
624
800
|
});
|
|
625
|
-
setInterval(function ping() {
|
|
801
|
+
const pingInterval = setInterval(function ping() {
|
|
626
802
|
wss.clients.forEach(function each(client) {
|
|
627
|
-
if (client.readyState ===
|
|
803
|
+
if (client.readyState === import_ws2.WebSocket.OPEN) {
|
|
628
804
|
client.send(JSON.stringify({ type: "ping", args: [] }));
|
|
629
805
|
}
|
|
630
806
|
});
|
|
631
807
|
}, 1e4);
|
|
808
|
+
pingInterval.unref?.();
|
|
632
809
|
wss.on("connection", function connection(ws) {
|
|
633
810
|
console.log("WebSocket connection established");
|
|
634
811
|
ws.on("error", console.error);
|
|
635
812
|
ws.on("message", function message(data) {
|
|
636
813
|
try {
|
|
637
814
|
const json = JSON.parse(data.toString());
|
|
815
|
+
selectStorySyncEndpoint?.onSocketMessage(json, ws);
|
|
638
816
|
const msg = JSON.stringify(json);
|
|
639
817
|
wss.clients.forEach((wsClient) => {
|
|
640
|
-
if (wsClient !== ws && wsClient.readyState ===
|
|
818
|
+
if (wsClient !== ws && wsClient.readyState === import_ws2.WebSocket.OPEN) {
|
|
641
819
|
wsClient.send(msg);
|
|
642
820
|
}
|
|
643
821
|
});
|
|
@@ -645,6 +823,9 @@ function createChannelServer({
|
|
|
645
823
|
console.error(error);
|
|
646
824
|
}
|
|
647
825
|
});
|
|
826
|
+
ws.on("close", () => {
|
|
827
|
+
selectStorySyncEndpoint?.onSocketClose(ws);
|
|
828
|
+
});
|
|
648
829
|
});
|
|
649
830
|
}
|
|
650
831
|
httpServer.on("error", (error) => {
|
|
@@ -391,7 +391,10 @@ async function buildIndex({ configPath }) {
|
|
|
391
391
|
const { meta, stories } = result;
|
|
392
392
|
if (stories && stories.length > 0) {
|
|
393
393
|
for (const story of stories) {
|
|
394
|
-
const id = (0, import_csf.toId)(meta.title, story.name);
|
|
394
|
+
const id = story.id ?? (0, import_csf.toId)(meta.title, story.name);
|
|
395
|
+
if (!id) {
|
|
396
|
+
throw new Error(`Failed to generate id for story ${story.name} in file ${fileName}`);
|
|
397
|
+
}
|
|
395
398
|
index.entries[id] = {
|
|
396
399
|
type: "story",
|
|
397
400
|
subtype: "story",
|
|
@@ -567,7 +570,7 @@ var path2 = __toESM(require("path"));
|
|
|
567
570
|
var import_generate = __toESM(require_generate());
|
|
568
571
|
|
|
569
572
|
// src/metro/channelServer.ts
|
|
570
|
-
var
|
|
573
|
+
var import_ws2 = require("ws");
|
|
571
574
|
var import_node_http = require("http");
|
|
572
575
|
init_buildIndex();
|
|
573
576
|
|
|
@@ -778,6 +781,168 @@ function createMcpHandler(configPath, wss) {
|
|
|
778
781
|
return { handleMcpRequest, preInit };
|
|
779
782
|
}
|
|
780
783
|
|
|
784
|
+
// src/metro/selectStorySyncEndpoint.ts
|
|
785
|
+
var import_ws = require("ws");
|
|
786
|
+
var SELECT_STORY_SYNC_ROUTE = "/select-story-sync/";
|
|
787
|
+
var SELECT_STORY_SYNC_TIMEOUT_MS = 1e3;
|
|
788
|
+
var LAST_RENDERED_STORY_TIMEOUT_MS = 500;
|
|
789
|
+
function getRenderedStoryId(event) {
|
|
790
|
+
if (!event || typeof event !== "object") {
|
|
791
|
+
return null;
|
|
792
|
+
}
|
|
793
|
+
const { type, args } = event;
|
|
794
|
+
if (type !== "storyRendered" || !Array.isArray(args) || args.length === 0) {
|
|
795
|
+
return null;
|
|
796
|
+
}
|
|
797
|
+
const [firstArg] = args;
|
|
798
|
+
if (typeof firstArg === "string") {
|
|
799
|
+
return firstArg;
|
|
800
|
+
}
|
|
801
|
+
if (firstArg && typeof firstArg === "object" && "storyId" in firstArg) {
|
|
802
|
+
const { storyId } = firstArg;
|
|
803
|
+
return typeof storyId === "string" ? storyId : null;
|
|
804
|
+
}
|
|
805
|
+
return null;
|
|
806
|
+
}
|
|
807
|
+
function parseStoryIdFromPath(pathname) {
|
|
808
|
+
const match = pathname.match(/^\/select-story-sync\/([^/]+)$/);
|
|
809
|
+
if (!match) {
|
|
810
|
+
return null;
|
|
811
|
+
}
|
|
812
|
+
try {
|
|
813
|
+
const storyId = decodeURIComponent(match[1]);
|
|
814
|
+
return storyId || null;
|
|
815
|
+
} catch {
|
|
816
|
+
return null;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
function createSelectStorySyncEndpoint(wss) {
|
|
820
|
+
const pendingStorySelections = /* @__PURE__ */ new Map();
|
|
821
|
+
const lastRenderedStoryIdByClient = /* @__PURE__ */ new Map();
|
|
822
|
+
const waitForStoryRender = (storyId, timeoutMs) => {
|
|
823
|
+
let cancelSelection = () => {
|
|
824
|
+
};
|
|
825
|
+
let resolveWait = () => {
|
|
826
|
+
};
|
|
827
|
+
const promise = new Promise((resolve2, reject) => {
|
|
828
|
+
resolveWait = resolve2;
|
|
829
|
+
let selections = pendingStorySelections.get(storyId);
|
|
830
|
+
if (!selections) {
|
|
831
|
+
selections = /* @__PURE__ */ new Set();
|
|
832
|
+
pendingStorySelections.set(storyId, selections);
|
|
833
|
+
}
|
|
834
|
+
const cleanup = () => {
|
|
835
|
+
clearTimeout(selection.timeout);
|
|
836
|
+
selections.delete(selection);
|
|
837
|
+
if (selections.size === 0) {
|
|
838
|
+
pendingStorySelections.delete(storyId);
|
|
839
|
+
}
|
|
840
|
+
};
|
|
841
|
+
const selection = {
|
|
842
|
+
resolve: () => {
|
|
843
|
+
if (selection.settled) {
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
selection.settled = true;
|
|
847
|
+
cleanup();
|
|
848
|
+
resolve2();
|
|
849
|
+
},
|
|
850
|
+
timeout: setTimeout(() => {
|
|
851
|
+
if (selection.settled) {
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
selection.settled = true;
|
|
855
|
+
cleanup();
|
|
856
|
+
reject(new Error(`Story "${storyId}" did not render in time`));
|
|
857
|
+
}, timeoutMs),
|
|
858
|
+
settled: false
|
|
859
|
+
};
|
|
860
|
+
cancelSelection = () => {
|
|
861
|
+
if (selection.settled) {
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
selection.settled = true;
|
|
865
|
+
cleanup();
|
|
866
|
+
resolveWait();
|
|
867
|
+
};
|
|
868
|
+
selections.add(selection);
|
|
869
|
+
});
|
|
870
|
+
return {
|
|
871
|
+
promise,
|
|
872
|
+
cancel: cancelSelection
|
|
873
|
+
};
|
|
874
|
+
};
|
|
875
|
+
const resolveStorySelection = (storyId) => {
|
|
876
|
+
const selections = pendingStorySelections.get(storyId);
|
|
877
|
+
if (!selections) {
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
[...selections].forEach((selection) => selection.resolve());
|
|
881
|
+
};
|
|
882
|
+
const handleRequest = async (pathname, res) => {
|
|
883
|
+
const storyId = parseStoryIdFromPath(pathname);
|
|
884
|
+
if (!storyId) {
|
|
885
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
886
|
+
res.end(JSON.stringify({ success: false, error: "Invalid story id" }));
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
const waitForRender = waitForStoryRender(storyId, SELECT_STORY_SYNC_TIMEOUT_MS);
|
|
890
|
+
const message = JSON.stringify({
|
|
891
|
+
type: "setCurrentStory",
|
|
892
|
+
args: [{ viewMode: "story", storyId }]
|
|
893
|
+
});
|
|
894
|
+
wss.clients.forEach((wsClient) => {
|
|
895
|
+
if (wsClient.readyState === import_ws.WebSocket.OPEN) {
|
|
896
|
+
wsClient.send(message);
|
|
897
|
+
}
|
|
898
|
+
});
|
|
899
|
+
try {
|
|
900
|
+
const hasConnectedClientWithRenderedStory = [...wss.clients].some(
|
|
901
|
+
(client) => client.readyState === import_ws.WebSocket.OPEN && lastRenderedStoryIdByClient.get(client) === storyId
|
|
902
|
+
);
|
|
903
|
+
if (hasConnectedClientWithRenderedStory) {
|
|
904
|
+
const raceResult = await Promise.race([
|
|
905
|
+
waitForRender.promise.then(() => "rendered"),
|
|
906
|
+
new Promise((resolve2) => {
|
|
907
|
+
setTimeout(() => resolve2("alreadyRendered"), LAST_RENDERED_STORY_TIMEOUT_MS);
|
|
908
|
+
})
|
|
909
|
+
]);
|
|
910
|
+
if (raceResult === "alreadyRendered") {
|
|
911
|
+
waitForRender.cancel();
|
|
912
|
+
}
|
|
913
|
+
} else {
|
|
914
|
+
await waitForRender.promise;
|
|
915
|
+
}
|
|
916
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
917
|
+
res.end(JSON.stringify({ success: true, storyId }));
|
|
918
|
+
} catch (error) {
|
|
919
|
+
res.writeHead(408, { "Content-Type": "application/json" });
|
|
920
|
+
res.end(
|
|
921
|
+
JSON.stringify({
|
|
922
|
+
success: false,
|
|
923
|
+
storyId,
|
|
924
|
+
error: error instanceof Error ? error.message : String(error)
|
|
925
|
+
})
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
};
|
|
929
|
+
const onSocketMessage = (event, ws) => {
|
|
930
|
+
const renderedStoryId = getRenderedStoryId(event);
|
|
931
|
+
if (renderedStoryId) {
|
|
932
|
+
lastRenderedStoryIdByClient.set(ws, renderedStoryId);
|
|
933
|
+
resolveStorySelection(renderedStoryId);
|
|
934
|
+
}
|
|
935
|
+
};
|
|
936
|
+
const onSocketClose = (ws) => {
|
|
937
|
+
lastRenderedStoryIdByClient.delete(ws);
|
|
938
|
+
};
|
|
939
|
+
return {
|
|
940
|
+
handleRequest,
|
|
941
|
+
onSocketMessage,
|
|
942
|
+
onSocketClose
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
|
|
781
946
|
// src/metro/channelServer.ts
|
|
782
947
|
function createChannelServer({
|
|
783
948
|
port = 7007,
|
|
@@ -787,15 +952,17 @@ function createChannelServer({
|
|
|
787
952
|
websockets = true
|
|
788
953
|
}) {
|
|
789
954
|
const httpServer = (0, import_node_http.createServer)();
|
|
790
|
-
const wss = websockets ? new
|
|
955
|
+
const wss = websockets ? new import_ws2.WebSocketServer({ server: httpServer }) : null;
|
|
791
956
|
const mcpServer = experimental_mcp ? createMcpHandler(configPath, wss ?? void 0) : null;
|
|
957
|
+
const selectStorySyncEndpoint = wss ? createSelectStorySyncEndpoint(wss) : null;
|
|
792
958
|
httpServer.on("request", async (req, res) => {
|
|
959
|
+
const requestUrl = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
793
960
|
if (req.method === "OPTIONS") {
|
|
794
961
|
res.writeHead(204);
|
|
795
962
|
res.end();
|
|
796
963
|
return;
|
|
797
964
|
}
|
|
798
|
-
if (req.method === "GET" &&
|
|
965
|
+
if (req.method === "GET" && requestUrl.pathname === "/index.json") {
|
|
799
966
|
try {
|
|
800
967
|
const index = await buildIndex({ configPath });
|
|
801
968
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
@@ -807,7 +974,7 @@ function createChannelServer({
|
|
|
807
974
|
}
|
|
808
975
|
return;
|
|
809
976
|
}
|
|
810
|
-
if (req.method === "POST" &&
|
|
977
|
+
if (req.method === "POST" && requestUrl.pathname === "/send-event") {
|
|
811
978
|
if (!wss) {
|
|
812
979
|
res.writeHead(503, { "Content-Type": "application/json" });
|
|
813
980
|
res.end(JSON.stringify({ success: false, error: "WebSockets are disabled" }));
|
|
@@ -831,7 +998,16 @@ function createChannelServer({
|
|
|
831
998
|
});
|
|
832
999
|
return;
|
|
833
1000
|
}
|
|
834
|
-
if (
|
|
1001
|
+
if (req.method === "POST" && requestUrl.pathname.startsWith(SELECT_STORY_SYNC_ROUTE)) {
|
|
1002
|
+
if (!selectStorySyncEndpoint) {
|
|
1003
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
1004
|
+
res.end(JSON.stringify({ success: false, error: "WebSockets are disabled" }));
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
await selectStorySyncEndpoint.handleRequest(requestUrl.pathname, res);
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
if (mcpServer && requestUrl.pathname === "/mcp" && (req.method === "POST" || req.method === "GET")) {
|
|
835
1011
|
await mcpServer.handleMcpRequest(req, res);
|
|
836
1012
|
return;
|
|
837
1013
|
}
|
|
@@ -841,22 +1017,24 @@ function createChannelServer({
|
|
|
841
1017
|
if (wss) {
|
|
842
1018
|
wss.on("error", () => {
|
|
843
1019
|
});
|
|
844
|
-
setInterval(function ping() {
|
|
1020
|
+
const pingInterval = setInterval(function ping() {
|
|
845
1021
|
wss.clients.forEach(function each(client) {
|
|
846
|
-
if (client.readyState ===
|
|
1022
|
+
if (client.readyState === import_ws2.WebSocket.OPEN) {
|
|
847
1023
|
client.send(JSON.stringify({ type: "ping", args: [] }));
|
|
848
1024
|
}
|
|
849
1025
|
});
|
|
850
1026
|
}, 1e4);
|
|
1027
|
+
pingInterval.unref?.();
|
|
851
1028
|
wss.on("connection", function connection(ws) {
|
|
852
1029
|
console.log("WebSocket connection established");
|
|
853
1030
|
ws.on("error", console.error);
|
|
854
1031
|
ws.on("message", function message(data) {
|
|
855
1032
|
try {
|
|
856
1033
|
const json = JSON.parse(data.toString());
|
|
1034
|
+
selectStorySyncEndpoint?.onSocketMessage(json, ws);
|
|
857
1035
|
const msg = JSON.stringify(json);
|
|
858
1036
|
wss.clients.forEach((wsClient) => {
|
|
859
|
-
if (wsClient !== ws && wsClient.readyState ===
|
|
1037
|
+
if (wsClient !== ws && wsClient.readyState === import_ws2.WebSocket.OPEN) {
|
|
860
1038
|
wsClient.send(msg);
|
|
861
1039
|
}
|
|
862
1040
|
});
|
|
@@ -864,6 +1042,9 @@ function createChannelServer({
|
|
|
864
1042
|
console.error(error);
|
|
865
1043
|
}
|
|
866
1044
|
});
|
|
1045
|
+
ws.on("close", () => {
|
|
1046
|
+
selectStorySyncEndpoint?.onSocketClose(ws);
|
|
1047
|
+
});
|
|
867
1048
|
});
|
|
868
1049
|
}
|
|
869
1050
|
httpServer.on("error", (error) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@storybook/react-native",
|
|
3
|
-
"version": "10.3.0-next.
|
|
3
|
+
"version": "10.3.0-next.5",
|
|
4
4
|
"description": "A better way to develop React Native Components for your app",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -44,10 +44,10 @@
|
|
|
44
44
|
],
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@storybook/mcp": "^0.4.1",
|
|
47
|
-
"@storybook/react": "10.3.0-alpha.
|
|
48
|
-
"@storybook/react-native-theming": "^10.3.0-next.
|
|
49
|
-
"@storybook/react-native-ui": "^10.3.0-next.
|
|
50
|
-
"@storybook/react-native-ui-common": "^10.3.0-next.
|
|
47
|
+
"@storybook/react": "10.3.0-alpha.14",
|
|
48
|
+
"@storybook/react-native-theming": "^10.3.0-next.5",
|
|
49
|
+
"@storybook/react-native-ui": "^10.3.0-next.5",
|
|
50
|
+
"@storybook/react-native-ui-common": "^10.3.0-next.5",
|
|
51
51
|
"@tmcp/adapter-valibot": "^0.1.4",
|
|
52
52
|
"@tmcp/transport-http": "^0.8.0",
|
|
53
53
|
"commander": "^14.0.2",
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"jotai": "^2.17.1",
|
|
72
72
|
"react": "19.2.0",
|
|
73
73
|
"react-native": "0.83.2",
|
|
74
|
-
"storybook": "10.3.0-alpha.
|
|
74
|
+
"storybook": "10.3.0-alpha.14",
|
|
75
75
|
"test-renderer": "^0.14.0",
|
|
76
76
|
"tsup": "^8.5.0",
|
|
77
77
|
"typescript": "~5.9.3"
|
package/readme.md
CHANGED
|
@@ -574,6 +574,7 @@ This repo includes agent skills for setting up and working with Storybook for Re
|
|
|
574
574
|
|
|
575
575
|
- **writing-react-native-storybook-stories** - Guides Claude on writing stories using Component Story Format (CSF), including controls, addons, decorators, parameters, and portable stories
|
|
576
576
|
- **setup-react-native-storybook** - Guides Claude through adding Storybook to your project, covering Expo, Expo Router, React Native CLI, and Re.Pack setups
|
|
577
|
+
- **upgrading-react-native-storybook** - Guides Claude through incremental React Native Storybook upgrades, split by supported migration paths from 5.3.x through 10.x, including converting remaining `storiesOf` stories to CSF during the 6.5.x to 7.6.x migration
|
|
577
578
|
|
|
578
579
|
### Installation
|
|
579
580
|
|