@storybook/react-native 10.3.0-next.3 → 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.
@@ -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 import_ws = require("ws");
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 import_ws.WebSocketServer({ server: httpServer }) : null;
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" && req.url === "/index.json") {
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" && req.url === "/send-event") {
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 (mcpServer && req.url === "/mcp" && (req.method === "POST" || req.method === "GET")) {
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 === import_ws.WebSocket.OPEN) {
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 === import_ws.WebSocket.OPEN) {
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 import_ws = require("ws");
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 import_ws.WebSocketServer({ server: httpServer }) : null;
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" && req.url === "/index.json") {
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" && req.url === "/send-event") {
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 (mcpServer && req.url === "/mcp" && (req.method === "POST" || req.method === "GET")) {
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 === import_ws.WebSocket.OPEN) {
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 === import_ws.WebSocket.OPEN) {
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 import_ws = require("ws");
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 import_ws.WebSocketServer({ server: httpServer }) : null;
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" && req.url === "/index.json") {
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" && req.url === "/send-event") {
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 (mcpServer && req.url === "/mcp" && (req.method === "POST" || req.method === "GET")) {
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 === import_ws.WebSocket.OPEN) {
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 === import_ws.WebSocket.OPEN) {
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",
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.12",
48
- "@storybook/react-native-theming": "^10.3.0-next.3",
49
- "@storybook/react-native-ui": "^10.3.0-next.3",
50
- "@storybook/react-native-ui-common": "^10.3.0-next.3",
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.12",
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