@storybook/react-native
Version:
A better way to develop React Native Components for your app
858 lines (819 loc) • 30.4 kB
JavaScript
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __esm = (fn, res) => function __init() {
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
};
var __commonJS = (cb, mod) => function __require() {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// scripts/common.js
var require_common = __commonJS({
"scripts/common.js"(exports2, module2) {
var { globToRegexp } = require("storybook/internal/common");
var path2 = require("path");
var fs = require("fs");
var cwd2 = process.cwd();
var toRequireContext = (specifier) => {
const { directory, files } = specifier;
const match = globToRegexp(`./${files}`);
return {
path: directory,
recursive: files.includes("**") || files.split("/").length > 1,
match
};
};
var supportedExtensions = ["js", "jsx", "ts", "tsx", "cjs", "mjs"];
function getFilePathExtension({ configPath }, fileName) {
for (const ext of supportedExtensions) {
const filePath = path2.resolve(cwd2, configPath, `${fileName}.${ext}`);
if (fs.existsSync(filePath)) {
return ext;
}
}
return null;
}
function getFilePathWithExtension2({ configPath }, fileName) {
for (const ext of supportedExtensions) {
const filePath = path2.resolve(cwd2, configPath, `${fileName}.${ext}`);
if (fs.existsSync(filePath)) {
return filePath;
}
}
return null;
}
function ensureRelativePathHasDot2(relativePath) {
return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
}
function getPreviewExists({ configPath }) {
return !!getFilePathExtension({ configPath }, "preview");
}
function resolveAddonFile(addon, file, extensions = ["js", "mjs", "ts"], configPath) {
if (!addon || typeof addon !== "string") return null;
const resolvePaths = { paths: [cwd2] };
try {
const basePath = `${addon}/${file}`;
require.resolve(basePath, resolvePaths);
return basePath;
} catch (_error) {
}
for (const ext of extensions) {
try {
const filePath = `${addon}/${file}.${ext}`;
require.resolve(filePath, resolvePaths);
return filePath;
} catch (_error) {
}
}
if (addon.startsWith("./") || addon.startsWith("../")) {
try {
const extension = getFilePathExtension({ configPath }, `${addon}/${file}`);
if (extension) {
return `${addon}/${file}`;
}
} catch (_error) {
}
}
return null;
}
function getAddonName(addon) {
if (typeof addon === "string") return addon;
if (typeof addon === "object" && addon.name && typeof addon.name === "string") return addon.name;
console.error("Invalid addon configuration", addon);
return null;
}
module2.exports = {
toRequireContext,
getFilePathExtension,
ensureRelativePathHasDot: ensureRelativePathHasDot2,
getPreviewExists,
resolveAddonFile,
getAddonName,
getFilePathWithExtension: getFilePathWithExtension2
};
}
});
// src/metro/buildIndex.ts
var buildIndex_exports = {};
__export(buildIndex_exports, {
buildIndex: () => buildIndex
});
function ensureRelativePathHasDot(relativePath) {
return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
}
async function buildIndex({ configPath }) {
const main = await (0, import_common.loadMainConfig)({ configDir: configPath, cwd });
if (!main.stories || !Array.isArray(main.stories)) {
throw new Error("No stories found");
}
const storiesSpecifiers = (0, import_common.normalizeStories)(main.stories, {
configDir: configPath,
workingDir: cwd
});
const specifierStoryPaths = storiesSpecifiers.map((specifier) => {
return (0, import_glob.sync)(specifier.files, {
cwd: import_path.default.resolve(process.cwd(), specifier.directory),
absolute: true,
// default to always ignore (exclude) anything in node_modules
ignore: ["**/node_modules"]
}).map((storyPath) => {
const normalizePathForWindows = (str) => import_path.default.sep === "\\" ? str.replace(/\\/g, "/") : str;
return normalizePathForWindows(storyPath);
});
});
const csfStories = specifierStoryPaths.reduce(
(acc, specifierStoryPathList, specifierIndex) => {
const paths = specifierStoryPathList.map((storyPath) => {
const code = (0, import_node_fs.readFileSync)(storyPath, { encoding: "utf-8" }).toString();
const relativePath = ensureRelativePathHasDot(import_path.default.posix.relative(cwd, storyPath));
return {
result: (0, import_csf_tools.loadCsf)(code, {
fileName: storyPath,
makeTitle: (userTitle) => makeTitle(relativePath, storiesSpecifiers[specifierIndex], userTitle)
}).parse(),
specifier: storiesSpecifiers[specifierIndex],
fileName: relativePath
};
});
return [...acc, ...paths];
},
new Array()
);
const index = {
v: 5,
entries: {}
};
for (const { result, specifier, fileName } of csfStories) {
const { meta, stories } = result;
if (stories && stories.length > 0) {
for (const story of stories) {
const id = story.id ?? (0, import_csf.toId)(meta.title, story.name);
if (!id) {
throw new Error(`Failed to generate id for story ${story.name} in file ${fileName}`);
}
index.entries[id] = {
type: "story",
subtype: "story",
id,
name: story.name,
title: meta.title,
importPath: `${specifier.directory}/${import_path.default.posix.relative(specifier.directory, fileName)}`,
tags: ["story"]
};
}
} else {
console.log(`No stories found for ${fileName}`);
}
}
try {
const previewPath = (0, import_common2.getFilePathWithExtension)({ configPath }, "preview");
const previewSourceCode = (0, import_node_fs.readFileSync)(previewPath, { encoding: "utf-8" }).toString();
const storySort = (0, import_csf_tools.getStorySortParameter)(previewSourceCode);
const sortableStories = Object.values(index.entries);
(0, import_preview_api.sortStoriesV7)(
sortableStories,
storySort,
sortableStories.map((entry) => entry.importPath)
);
const sorted = sortableStories.reduce(
(acc, item) => {
acc[item.id] = item;
return acc;
},
{}
);
return { v: 5, entries: sorted };
} catch {
console.warn("Failed to sort stories, using unordered index");
return index;
}
}
var import_common, import_node_fs, import_glob, import_path, import_csf_tools, import_csf, import_preview_api, import_common2, cwd, makeTitle;
var init_buildIndex = __esm({
"src/metro/buildIndex.ts"() {
import_common = require("storybook/internal/common");
import_node_fs = require("fs");
import_glob = require("glob");
import_path = __toESM(require("path"));
import_csf_tools = require("storybook/internal/csf-tools");
import_csf = require("storybook/internal/csf");
import_preview_api = require("storybook/internal/preview-api");
import_common2 = __toESM(require_common());
cwd = process.cwd();
makeTitle = (fileName, specifier, userTitle) => {
const title = (0, import_preview_api.userOrAutoTitleFromSpecifier)(fileName, specifier, userTitle);
if (title) {
return title.replace("./", "");
} else if (userTitle) {
return userTitle.replace("./", "");
} else {
console.error("Could not generate title!!");
process.exit(1);
}
};
}
});
// src/metro/manifest/storyInstructions.ts
var storyInstructions_exports = {};
__export(storyInstructions_exports, {
storyInstructions: () => storyInstructions
});
var storyInstructions;
var init_storyInstructions = __esm({
"src/metro/manifest/storyInstructions.ts"() {
storyInstructions = `# Writing React Native UI Components
When writing UI, prefer breaking larger components up into smaller parts.
ALWAYS write a Storybook story for any component written. If editing a component, ensure appropriate changes have been made to stories for that component.
## How to write good stories
Goal: Cover every distinct piece of business logic and state the component can reach (happy paths, error/edge states, loading, permissions/roles, empty states, variations from props/context). Avoid redundant stories that show the same logic.
Interactivity: For interactive components, create separate stories that demonstrate each interaction state. Use \`fn()\` from \`storybook/test\` to mock callback props so you can verify they are wired up correctly.
Data/setup: Provide realistic props, state, and mocked data. Include meaningful labels/text to make behaviors observable. Stub network/services with deterministic fixtures; keep stories reliable.
Variants to consider (pick only those that change behavior): default vs. alternate themes; loading vs. loaded vs. empty vs. error; validated vs. invalid input; permissions/roles/capabilities; feature flags; size/density/layout variants that alter logic.
Accessibility: Use semantic roles/labels where applicable.
Naming/structure: Use clear story names that describe the scenario ("Error state after failed submit"). Group related variants logically; don't duplicate.
Imports/format: Import Meta/StoryObj from the framework package. Keep stories minimal\u2014only what's needed to demonstrate behavior.
## React Native Storybook Essentials
### Framework and Renderer
React Native Storybook uses \`@storybook/react-native\` as the framework. Stories use the same CSF (Component Story Format) as web Storybook.
### Meta and StoryObj imports
\`\`\`ts
import type { Meta, StoryObj } from '@storybook/react-native';
\`\`\`
### Story file structure
\`\`\`tsx
import type { Meta, StoryObj } from '@storybook/react-native';
import { MyComponent } from './MyComponent';
const meta: Meta<typeof MyComponent> = {
title: 'Components/MyComponent',
component: MyComponent,
args: {
// default args
},
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
export const WithCustomProps: Story = {
args: {
label: 'Custom Label',
variant: 'secondary',
},
};
\`\`\`
### Global State Changes
The \`globals\` annotation has been renamed to \`initialGlobals\`:
\`\`\`diff
// .rnstorybook/preview.js
export default {
- globals: { theme: 'light' }
+ initialGlobals: { theme: 'light' }
};
\`\`\`
### React Native Specific Considerations
- The config directory is \`.rnstorybook\` (not \`.storybook\`)
- Stories run on-device (iOS/Android), not in a browser
- Use React Native components (\`View\`, \`Text\`, \`Pressable\`, etc.), not HTML elements
- \`StyleSheet\` or inline styles instead of CSS
- No DOM APIs \u2014 use React Native's layout system (Flexbox)
- For navigation-dependent components, mock the navigation context
- For platform-specific stories, use \`Platform.OS\` checks or separate story files
- Test on both iOS and Android when possible
### Key Requirements
- **Node.js 20+**, **TypeScript 4.9+**
- React Native 0.72+
- Storybook 10+
`;
}
});
// src/node.ts
var node_exports = {};
__export(node_exports, {
buildIndex: () => buildIndex,
createChannelServer: () => createChannelServer
});
module.exports = __toCommonJS(node_exports);
// src/metro/channelServer.ts
var import_ws2 = require("ws");
var import_node_http = require("http");
var import_node_https = require("https");
init_buildIndex();
// src/metro/mcpServer.ts
var import_consumers = require("stream/consumers");
function toHeaderEntries(nodeHeaders) {
const entries = [];
for (const [key, value] of Object.entries(nodeHeaders)) {
if (value === void 0) continue;
entries.push([key, Array.isArray(value) ? value.join(", ") : value]);
}
return entries;
}
async function incomingMessageToWebRequest(req) {
const host = req.headers.host || "localhost";
const isTLS = "encrypted" in req.socket && req.socket.encrypted;
const protocol = isTLS ? "https" : "http";
const url = new URL(req.url || "/", `${protocol}://${host}`);
const bodyBuffer = await (0, import_consumers.buffer)(req);
return new Request(url, {
method: req.method,
headers: toHeaderEntries(req.headers),
body: bodyBuffer.length > 0 ? new Uint8Array(bodyBuffer) : void 0
});
}
async function webResponseToServerResponse(webResponse, nodeResponse) {
nodeResponse.statusCode = webResponse.status;
webResponse.headers.forEach((value, key) => {
nodeResponse.setHeader(key, value);
});
if (webResponse.body) {
const reader = webResponse.body.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
nodeResponse.write(value);
}
} finally {
reader.releaseLock();
}
}
nodeResponse.end();
}
function createMcpHandler(configPath, wss) {
let handler = null;
let initPromise = null;
async function init() {
if (handler) return;
if (initPromise) {
await initPromise;
return;
}
initPromise = (async () => {
try {
const [
{ McpServer },
{ ValibotJsonSchemaAdapter },
{ HttpTransport },
{ addListAllDocumentationTool, addGetDocumentationTool, addGetStoryDocumentationTool },
{ storyInstructions: storyInstructions2 },
{ buildIndex: buildIndex2 },
valibot,
{ experimental_manifests }
] = await Promise.all([
import("tmcp"),
import("@tmcp/adapter-valibot"),
import("@tmcp/transport-http"),
import("@storybook/mcp"),
Promise.resolve().then(() => (init_storyInstructions(), storyInstructions_exports)),
Promise.resolve().then(() => (init_buildIndex(), buildIndex_exports)),
import("valibot"),
import("@storybook/react/preset")
]);
const manifestProvider = async (_request, manifestPath) => {
if (manifestPath.includes("docs.json")) {
throw new Error("Docs manifest not available in React Native Storybook");
}
const index = await buildIndex2({ configPath });
const entries = Object.values(index.entries);
const manifest = await experimental_manifests({}, { manifestEntries: entries });
return JSON.stringify(manifest.components);
};
const server = new McpServer(
{
name: "@storybook/react-native",
version: "1.0.0",
description: "Storybook React Native MCP server"
},
{
adapter: new ValibotJsonSchemaAdapter(),
capabilities: {
tools: { listChanged: true }
}
}
).withContext();
addListAllDocumentationTool(server);
addGetDocumentationTool(server);
addGetStoryDocumentationTool(server);
server.tool(
{
name: "get-storybook-story-instructions",
title: "React Native Storybook Story Instructions",
description: "Get instructions for writing React Native Storybook stories. Call this before creating or modifying story files (.stories.tsx, .stories.ts)."
},
async () => ({
content: [{ type: "text", text: storyInstructions2 }]
})
);
if (wss) {
const broadcastEvent = (event) => {
const message = JSON.stringify(event);
wss.clients.forEach((client) => {
if (client.readyState === 1) {
client.send(message);
}
});
};
server.tool(
{
name: "select-story",
title: "Select Story",
description: 'Select and display a story on the connected device. Use the story ID in the format "title--name" (e.g. "button--primary"). Use the list-all-documentation tool to discover available components and stories.',
schema: valibot.object({ storyId: valibot.string() })
},
async ({ storyId }) => {
try {
const index = await buildIndex2({ configPath });
if (!index.entries[storyId]) {
const availableIds = Object.keys(index.entries).slice(0, 10);
return {
content: [
{
type: "text",
text: `Story "${storyId}" not found. Available stories include: ${availableIds.join(", ")}` + (Object.keys(index.entries).length > 10 ? ", ..." : "")
}
],
isError: true
};
}
broadcastEvent({
type: "setCurrentStory",
args: [{ storyId, viewMode: "story" }]
});
const entry = index.entries[storyId];
return {
content: [
{
type: "text",
text: `Selected story "${entry.name}" (${entry.title}) on connected devices.`
}
]
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Failed to select story: ${error instanceof Error ? error.message : String(error)}`
}
],
isError: true
};
}
}
);
}
const transport = new HttpTransport(server, { path: null });
handler = (req) => transport.respond(req, {
request: req,
manifestProvider
});
console.log("[Storybook] MCP server initialized");
} catch (error) {
initPromise = null;
console.error("[Storybook] Failed to initialize MCP server:", error);
throw error;
}
})();
await initPromise;
}
async function handleMcpRequest(req, res) {
try {
await init();
if (!handler) {
res.writeHead(500, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "MCP handler not initialized" }));
return;
}
const webRequest = await incomingMessageToWebRequest(req);
const webResponse = await handler(webRequest);
await webResponseToServerResponse(webResponse, res);
} catch (error) {
console.error("[Storybook] MCP request failed:", error);
res.writeHead(500, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "MCP request failed" }));
}
}
function preInit() {
init().catch(
(e) => console.warn("[Storybook] MCP pre-initialization failed (will retry on first request):", e)
);
}
return { handleMcpRequest, preInit };
}
// src/metro/selectStorySyncEndpoint.ts
var import_ws = require("ws");
var SELECT_STORY_SYNC_ROUTE = "/select-story-sync/";
var SELECT_STORY_SYNC_TIMEOUT_MS = 1e3;
var LAST_RENDERED_STORY_TIMEOUT_MS = 500;
function getRenderedStoryId(event) {
if (!event || typeof event !== "object") {
return null;
}
const { type, args } = event;
if (type !== "storyRendered" || !Array.isArray(args) || args.length === 0) {
return null;
}
const [firstArg] = args;
if (typeof firstArg === "string") {
return firstArg;
}
if (firstArg && typeof firstArg === "object" && "storyId" in firstArg) {
const { storyId } = firstArg;
return typeof storyId === "string" ? storyId : null;
}
return null;
}
function parseStoryIdFromPath(pathname) {
const match = pathname.match(/^\/select-story-sync\/([^/]+)$/);
if (!match) {
return null;
}
try {
const storyId = decodeURIComponent(match[1]);
return storyId || null;
} catch {
return null;
}
}
function createSelectStorySyncEndpoint(wss) {
const pendingStorySelections = /* @__PURE__ */ new Map();
const lastRenderedStoryIdByClient = /* @__PURE__ */ new Map();
const waitForStoryRender = (storyId, timeoutMs) => {
let cancelSelection = () => {
};
let resolveWait = () => {
};
const promise = new Promise((resolve, reject) => {
resolveWait = resolve;
let selections = pendingStorySelections.get(storyId);
if (!selections) {
selections = /* @__PURE__ */ new Set();
pendingStorySelections.set(storyId, selections);
}
const cleanup = () => {
clearTimeout(selection.timeout);
selections.delete(selection);
if (selections.size === 0) {
pendingStorySelections.delete(storyId);
}
};
const selection = {
resolve: () => {
if (selection.settled) {
return;
}
selection.settled = true;
cleanup();
resolve();
},
timeout: setTimeout(() => {
if (selection.settled) {
return;
}
selection.settled = true;
cleanup();
reject(new Error(`Story "${storyId}" did not render in time`));
}, timeoutMs),
settled: false
};
cancelSelection = () => {
if (selection.settled) {
return;
}
selection.settled = true;
cleanup();
resolveWait();
};
selections.add(selection);
});
return {
promise,
cancel: cancelSelection
};
};
const resolveStorySelection = (storyId) => {
const selections = pendingStorySelections.get(storyId);
if (!selections) {
return;
}
[...selections].forEach((selection) => selection.resolve());
};
const handleRequest = async (pathname, res) => {
const storyId = parseStoryIdFromPath(pathname);
if (!storyId) {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ success: false, error: "Invalid story id" }));
return;
}
const waitForRender = waitForStoryRender(storyId, SELECT_STORY_SYNC_TIMEOUT_MS);
const message = JSON.stringify({
type: "setCurrentStory",
args: [{ viewMode: "story", storyId }]
});
wss.clients.forEach((wsClient) => {
if (wsClient.readyState === import_ws.WebSocket.OPEN) {
wsClient.send(message);
}
});
try {
const hasConnectedClientWithRenderedStory = [...wss.clients].some(
(client) => client.readyState === import_ws.WebSocket.OPEN && lastRenderedStoryIdByClient.get(client) === storyId
);
if (hasConnectedClientWithRenderedStory) {
const raceResult = await Promise.race([
waitForRender.promise.then(() => "rendered"),
new Promise((resolve) => {
setTimeout(() => resolve("alreadyRendered"), LAST_RENDERED_STORY_TIMEOUT_MS);
})
]);
if (raceResult === "alreadyRendered") {
waitForRender.cancel();
}
} else {
await waitForRender.promise;
}
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ success: true, storyId }));
} catch (error) {
res.writeHead(408, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
success: false,
storyId,
error: error instanceof Error ? error.message : String(error)
})
);
}
};
const onSocketMessage = (event, ws) => {
const renderedStoryId = getRenderedStoryId(event);
if (renderedStoryId) {
lastRenderedStoryIdByClient.set(ws, renderedStoryId);
resolveStorySelection(renderedStoryId);
}
};
const onSocketClose = (ws) => {
lastRenderedStoryIdByClient.delete(ws);
};
return {
handleRequest,
onSocketMessage,
onSocketClose
};
}
// src/metro/channelServer.ts
function createChannelServer({
port = 7007,
host = void 0,
configPath,
experimental_mcp = false,
websockets = true,
secured = false,
ssl
}) {
if (secured && (!ssl?.key || !ssl?.cert)) {
throw new Error("[Storybook] Secure channel server requires both `ssl.key` and `ssl.cert`.");
}
const httpServer = secured ? (0, import_node_https.createServer)(ssl) : (0, import_node_http.createServer)();
const wss = websockets ? new import_ws2.WebSocketServer({ server: httpServer }) : null;
const mcpServer = experimental_mcp ? createMcpHandler(configPath, wss ?? void 0) : null;
const selectStorySyncEndpoint = wss ? createSelectStorySyncEndpoint(wss) : null;
httpServer.on("request", async (req, res) => {
const protocol = "encrypted" in req.socket && req.socket.encrypted ? "https" : "http";
const requestUrl = new URL(req.url ?? "/", `${protocol}://${req.headers.host ?? "localhost"}`);
if (req.method === "OPTIONS") {
res.writeHead(204);
res.end();
return;
}
if (req.method === "GET" && requestUrl.pathname === "/index.json") {
try {
const index = await buildIndex({ configPath });
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(index));
} catch (error) {
console.error("Failed to build index:", error);
res.writeHead(500, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Failed to build story index" }));
}
return;
}
if (req.method === "POST" && requestUrl.pathname === "/send-event") {
if (!wss) {
res.writeHead(503, { "Content-Type": "application/json" });
res.end(JSON.stringify({ success: false, error: "WebSockets are disabled" }));
return;
}
let body = "";
req.on("data", (chunk) => {
body += chunk.toString();
});
req.on("end", () => {
try {
const json = JSON.parse(body);
wss.clients.forEach((wsClient) => wsClient.send(JSON.stringify(json)));
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ success: true }));
} catch (error) {
console.error("Failed to parse event:", error);
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ success: false, error: "Invalid JSON" }));
}
});
return;
}
if (req.method === "POST" && requestUrl.pathname.startsWith(SELECT_STORY_SYNC_ROUTE)) {
if (!selectStorySyncEndpoint) {
res.writeHead(503, { "Content-Type": "application/json" });
res.end(JSON.stringify({ success: false, error: "WebSockets are disabled" }));
return;
}
await selectStorySyncEndpoint.handleRequest(requestUrl.pathname, res);
return;
}
if (mcpServer && requestUrl.pathname === "/mcp" && (req.method === "POST" || req.method === "GET")) {
await mcpServer.handleMcpRequest(req, res);
return;
}
res.writeHead(404, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Not found" }));
});
if (wss) {
wss.on("error", () => {
});
const pingInterval = setInterval(function ping() {
wss.clients.forEach(function each(client) {
if (client.readyState === import_ws2.WebSocket.OPEN) {
client.send(JSON.stringify({ type: "ping", args: [] }));
}
});
}, 1e4);
pingInterval.unref?.();
wss.on("connection", function connection(ws) {
console.log("WebSocket connection established");
ws.on("error", console.error);
ws.on("message", function message(data) {
try {
const json = JSON.parse(data.toString());
selectStorySyncEndpoint?.onSocketMessage(json, ws);
const msg = JSON.stringify(json);
wss.clients.forEach((wsClient) => {
if (wsClient !== ws && wsClient.readyState === import_ws2.WebSocket.OPEN) {
wsClient.send(msg);
}
});
} catch (error) {
console.error(error);
}
});
ws.on("close", () => {
selectStorySyncEndpoint?.onSocketClose(ws);
});
});
}
httpServer.on("error", (error) => {
if (error.code === "EADDRINUSE") {
console.warn(
`[Storybook] Port ${port} is already in use. The channel server will not start. Another instance may already be running.`
);
} else {
console.error(`[Storybook] Channel server error:`, error);
}
});
httpServer.listen(port, host, () => {
const protocol = wss ? secured ? "WSS" : "WebSocket" : secured ? "HTTPS" : "HTTP";
console.log(`${protocol} server listening on ${host ?? "localhost"}:${port}`);
});
mcpServer?.preInit();
return wss;
}
// src/node.ts
init_buildIndex();
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
buildIndex,
createChannelServer
});