UNPKG

@storybook/react-native

Version:

A better way to develop React Native Components for your app

1,224 lines (1,175 loc) 42.7 kB
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 path3 = 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 = path3.resolve(cwd2, configPath, `${fileName}.${ext}`); if (fs.existsSync(filePath)) { return ext; } } return null; } function getFilePathWithExtension2({ configPath }, fileName) { for (const ext of supportedExtensions) { const filePath = path3.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 }; } }); // scripts/require-interop.js var require_require_interop = __commonJS({ "scripts/require-interop.js"(exports2, module2) { var registered = false; function interopRequireDefault(filePath) { const hasEsbuildBeenRegistered = !!require("module")._extensions[".ts"]; if (registered === false && !hasEsbuildBeenRegistered) { const { register } = require("esbuild-register/dist/node"); registered = true; register({ target: `node${process.version.slice(1)}`, format: "cjs", hookIgnoreNodeModules: true, // Some frameworks, like Stylus, rely on the 'name' property of classes or functions // https://github.com/storybookjs/storybook/issues/19049 keepNames: true, tsconfigRaw: `{ "compilerOptions": { "strict": false, "skipLibCheck": true, }, }` }); } const result = require(filePath); const isES6DefaultExported = typeof result === "object" && result !== null && typeof result.default !== "undefined"; return isES6DefaultExported ? result.default : result; } module2.exports = { interopRequireDefault }; } }); // scripts/generate.js var require_generate = __commonJS({ "scripts/generate.js"(exports2, module2) { var { toRequireContext, ensureRelativePathHasDot: ensureRelativePathHasDot2, getPreviewExists, resolveAddonFile, getAddonName } = require_common(); var { normalizeStories: normalizeStories2, globToRegexp, loadMainConfig: loadMainConfig2 } = require("storybook/internal/common"); var { interopRequireDefault } = require_require_interop(); var fs = require("fs"); var { networkInterfaces } = require("os"); var path3 = require("path"); var cwd2 = process.cwd(); var loadMain = async ({ configPath, cwd: cwd3 }) => { try { const main = await loadMainConfig2({ configDir: configPath, cwd: cwd3 }); return main; } catch { console.error("Error loading main config, trying fallback"); } const mainPathTs = path3.resolve(cwd3, configPath, `main.ts`); const mainPathJs = path3.resolve(cwd3, configPath, `main.js`); if (fs.existsSync(mainPathTs)) { return interopRequireDefault(mainPathTs); } else if (fs.existsSync(mainPathJs)) { return interopRequireDefault(mainPathJs); } else { throw new Error(`Main config file not found at ${mainPathTs} or ${mainPathJs}`); } }; function getLocalIPAddress() { const nets = networkInterfaces(); for (const name of Object.keys(nets)) { for (const net of nets[name]) { const familyV4Value = typeof net.family === "string" ? "IPv4" : 4; if (net.family === familyV4Value && !net.internal) { return net.address; } } } return "0.0.0.0"; } async function generate2({ configPath, useJs = false, docTools = true, host = void 0, port = void 0, secured = false }) { const channelHost = host === "auto" ? getLocalIPAddress() : host; const storybookRequiresLocation = path3.resolve( cwd2, configPath, `storybook.requires.${useJs ? "js" : "ts"}` ); const main = await loadMain({ configPath, cwd: cwd2 }); const storiesSpecifiers = normalizeStories2(main.stories, { configDir: configPath, workingDir: cwd2 }); const normalizedStories = storiesSpecifiers.map((specifier) => { const reg = globToRegexp(`./${specifier.files}`); const { path: p, recursive: r, match: m } = toRequireContext(specifier); const pathToStory = ensureRelativePathHasDot2(path3.posix.relative(configPath, p)); return `{ titlePrefix: "${specifier.titlePrefix}", directory: "${specifier.directory}", files: "${specifier.files}", importPathMatcher: /${reg.source}/, req: require.context( '${pathToStory}', ${r}, ${m} ), }`; }); const registeredAddons = []; for (const addon of main.addons) { const registerPath = resolveAddonFile( getAddonName(addon), "register", ["js", "mjs", "jsx", "ts", "tsx"], configPath ); if (registerPath) { registeredAddons.push(`import "${registerPath}";`); } } const docToolsAnnotation = 'require("@storybook/react-native/preview")'; const enhancers = []; if (docTools) { enhancers.push(docToolsAnnotation); } for (const addon of main.addons) { const previewPath = resolveAddonFile( getAddonName(addon), "preview", ["js", "mjs", "jsx", "ts", "tsx"], configPath ); if (previewPath) { enhancers.push(`require('${previewPath}')`); continue; } } let options = ""; let optionsVar = ""; const reactNativeOptions = main.reactNative; if (reactNativeOptions && typeof reactNativeOptions === "object") { optionsVar = `const options = ${JSON.stringify(reactNativeOptions, null, 2)}`; options = "options"; } let featuresAssignment = ""; const features = main.features; if (features && typeof features === "object") { const featureEntries = Object.entries(features).filter( ([, value]) => typeof value === "boolean" ); if (featureEntries.length > 0) { const assignments = featureEntries.map(([key, value]) => `globalThis.FEATURES.${key} = ${value};`).join("\n"); featuresAssignment = assignments; } } const previewExists = getPreviewExists({ configPath }); if (previewExists) { enhancers.unshift("require('./preview')"); } const annotations = `[ ${enhancers.join(",\n ")} ]`; const hasWebsocketConfig = host !== void 0 || port !== void 0 || secured; const websocketAssignmentLines = []; if (channelHost) { websocketAssignmentLines.push(`host: '${channelHost}',`); } if (hasWebsocketConfig) { websocketAssignmentLines.push(`port: ${port ?? 7007},`); websocketAssignmentLines.push(`secured: ${Boolean(secured)},`); } const globalTypes = ` declare global { var view: View; var STORIES: typeof normalizedStories; var STORYBOOK_WEBSOCKET: | { host?: string; port?: number; secured?: boolean } | undefined; var FEATURES: Features; } `; const fileContent = `/* do not change this file, it is auto generated by storybook. */ ${useJs ? "" : '/// <reference types="@storybook/react-native/metro-env" />\n'}import { start, updateView${useJs ? "" : ", View, type Features"} } from '@storybook/react-native'; ${registeredAddons.join("\n")} const normalizedStories = [ ${normalizedStories.join(",\n ")} ]; ${useJs ? "" : globalTypes} const annotations = ${annotations}; globalThis.STORIES = normalizedStories; ${hasWebsocketConfig ? `globalThis.STORYBOOK_WEBSOCKET = { ${websocketAssignmentLines.join("\n ")} };` : ""} module?.hot?.accept?.(); ${featuresAssignment ? ` ${featuresAssignment} ` : ""} ${optionsVar} if (!globalThis.view) { globalThis.view = start({ annotations, storyEntries: normalizedStories, ${options ? ` ${options},` : ""} }); } else { updateView(globalThis.view, annotations, normalizedStories${options ? `, ${options}` : ""}); } export const view${useJs ? "" : ": View"} = globalThis.view; `; fs.writeFileSync(storybookRequiresLocation, fileContent, { encoding: "utf8", flag: "w" }); } module2.exports = { generate: generate2 }; } }); // 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/metro/withStorybook.ts var withStorybook_exports = {}; __export(withStorybook_exports, { withStorybook: () => withStorybook }); module.exports = __toCommonJS(withStorybook_exports); var path2 = __toESM(require("path")); var import_generate = __toESM(require_generate()); var import_common3 = require("storybook/internal/common"); var import_telemetry = require("storybook/internal/telemetry"); // 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((resolve2, reject) => { resolveWait = resolve2; 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(); resolve2(); }, 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((resolve2) => { setTimeout(() => resolve2("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/metro/withStorybook.ts function withStorybook(config, options = { useJs: false, enabled: true, docTools: true, liteMode: false, configPath: path2.resolve(process.cwd(), "./.rnstorybook") }) { const { configPath = path2.resolve(process.cwd(), "./.rnstorybook"), websockets, useJs = false, enabled = true, docTools = true, liteMode = false, experimental_mcp = false } = options; const disableTelemetry = (0, import_common3.optionalEnvToBoolean)(process.env.STORYBOOK_DISABLE_TELEMETRY); if (!disableTelemetry && enabled) { const event = process.env.NODE_ENV === "production" ? "build" : "dev"; (0, import_telemetry.telemetry)(event, {}).catch((e) => { }); } if (!enabled) { return { ...config, resolver: { ...config.resolver, resolveRequest: (context, moduleName, platform) => { const resolveFunction = config?.resolver?.resolveRequest ? config.resolver.resolveRequest : context.resolveRequest; if (moduleName.startsWith("storybook") || moduleName.startsWith("@storybook")) { return { type: "empty" }; } if (moduleName === "tty" || moduleName === "os") { return { type: "empty" }; } const resolved = resolveFunction(context, moduleName, platform); const configIndexRegex = new RegExp(`${configPath}/index\\.(tsx?|jsx?)$`); if (resolved.filePath && configIndexRegex.test(resolved.filePath)) { return { filePath: path2.resolve(__dirname, "../stub.js"), type: "sourceFile" }; } if (resolved.filePath?.includes?.(configPath)) { return { type: "empty" }; } return resolved; } } }; } if (websockets || experimental_mcp) { const port = websockets === "auto" ? 7007 : websockets?.port ?? 7007; const host = websockets === "auto" ? "auto" : websockets?.host; const secured = Boolean(websockets && websockets !== "auto" && websockets.secured); createChannelServer({ port, host: host === "auto" ? void 0 : host, configPath, experimental_mcp, websockets: Boolean(websockets), secured, ssl: websockets && websockets !== "auto" ? { key: websockets.key, cert: websockets.cert, ca: websockets.ca, passphrase: websockets.passphrase } : void 0 }); if (websockets) { (0, import_generate.generate)({ configPath, useJs, docTools, host, port, secured }); } else { (0, import_generate.generate)({ configPath, useJs, docTools }); } } else { (0, import_generate.generate)({ configPath, useJs, docTools }); } return { ...config, transformer: { ...config.transformer, unstable_allowRequireContext: true }, resolver: { ...config.resolver, resolveRequest: (context, moduleName, platform) => { const resolveFunction = config?.resolver?.resolveRequest ? config.resolver.resolveRequest : context.resolveRequest; const shouldUseCustomResolveConfig = moduleName.startsWith("storybook") || moduleName.startsWith("@storybook") || moduleName.startsWith("uuid"); const theContext = shouldUseCustomResolveConfig ? { ...context, unstable_enablePackageExports: true, unstable_conditionNames: ["import"] } : context; const resolveResult = resolveFunction(theContext, moduleName, platform); if (resolveResult?.filePath?.includes?.("@storybook/react/template/cli")) { return { type: "empty" }; } if (moduleName === "tty" || moduleName === "os") { return { type: "empty" }; } if (liteMode && resolveResult?.filePath?.includes?.("@storybook/react-native-ui") && !resolveResult?.filePath?.includes?.("@storybook/react-native-ui-lite") && !resolveResult?.filePath?.includes?.("@storybook/react-native-ui-common")) { return { type: "empty" }; } return resolveResult; } } }; } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { withStorybook });