UNPKG

@storybook/react-native

Version:

A better way to develop React Native Components for your app

615 lines (597 loc) 21.1 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 __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 = 7007 }) { 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}/, ${useJs ? "" : "// @ts-ignore"} 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"; } const previewExists = getPreviewExists({ configPath }); if (previewExists) { enhancers.unshift("require('./preview')"); } const annotations = `[ ${enhancers.join(",\n ")} ]`; const globalTypes = ` declare global { var view: View; var STORIES: typeof normalizedStories; var STORYBOOK_WEBSOCKET: { host: string; port: number } | undefined; } `; const fileContent = `/* do not change this file, it is auto generated by storybook. */ import { start, updateView${useJs ? "" : ", View"} } from '@storybook/react-native'; ${registeredAddons.join("\n")} const normalizedStories = [ ${normalizedStories.join(",\n ")} ]; ${useJs ? "" : globalTypes} const annotations = ${annotations}; globalThis.STORIES = normalizedStories; ${channelHost ? `globalThis.STORYBOOK_WEBSOCKET = { host: '${channelHost}', port: ${port ?? 7007} };` : ""} ${useJs ? "" : "// @ts-ignore"} module?.hot?.accept?.(); ${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/repack/withStorybook.ts var withStorybook_exports = {}; __export(withStorybook_exports, { StorybookPlugin: () => StorybookPlugin }); module.exports = __toCommonJS(withStorybook_exports); var path2 = __toESM(require("path")); var import_generate = __toESM(require_generate()); // src/metro/channelServer.ts var import_ws = require("ws"); var import_node_http = require("http"); // src/metro/buildIndex.ts var import_common = require("storybook/internal/common"); var import_node_fs = require("fs"); var import_glob = require("glob"); var import_path = __toESM(require("path")); var import_csf_tools = require("storybook/internal/csf-tools"); var import_csf = require("storybook/internal/csf"); var import_preview_api = require("storybook/internal/preview-api"); var import_common2 = __toESM(require_common()); var cwd = process.cwd(); var 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); } }; 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 = (0, import_csf.toId)(meta.title, story.name); 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; } } // src/metro/channelServer.ts function createChannelServer({ port = 7007, host = void 0, configPath }) { const httpServer = (0, import_node_http.createServer)(async (req, res) => { if (req.method === "OPTIONS") { res.writeHead(204); res.end(); return; } if (req.method === "GET" && req.url === "/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" && req.url === "/send-event") { 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; } res.writeHead(404, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Not found" })); }); const wss = new import_ws.WebSocketServer({ server: httpServer }); wss.on("error", () => { }); setInterval(function ping() { wss.clients.forEach(function each(client) { if (client.readyState === import_ws.WebSocket.OPEN) { client.send(JSON.stringify({ type: "ping", args: [] })); } }); }, 1e4); 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()); wss.clients.forEach((wsClient) => wsClient.send(JSON.stringify(json))); } catch (error) { console.error(error); } }); }); 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, () => { console.log(`WebSocket server listening on ${host ?? "localhost"}:${port}`); }); return wss; } // src/repack/withStorybook.ts var StorybookPlugin = class { options; generated = false; serverStarted = false; constructor(options = {}) { this.options = { configPath: path2.resolve(process.cwd(), "./.rnstorybook"), enabled: true, useJs: false, docTools: true, liteMode: false, ...options }; } apply(compiler) { const { configPath, enabled, websockets, useJs, docTools, liteMode } = this.options; if (!enabled) { this.applyDisabled(compiler, configPath); return; } this.applyEnabled(compiler, { configPath, websockets, useJs, docTools, liteMode }); } /** * When enabled: generate storybook.requires, optionally start websocket server, * and set up liteMode aliases. */ applyEnabled(compiler, { configPath, websockets, useJs, docTools, liteMode }) { const port = websockets === "auto" ? 7007 : websockets?.port ?? 7007; const host = websockets === "auto" ? "auto" : websockets?.host; if (websockets && !this.serverStarted) { this.serverStarted = true; createChannelServer({ port, host: host === "auto" ? void 0 : host, configPath }); } compiler.hooks.beforeCompile.tapPromise("StorybookPlugin", async () => { if (this.generated) return; this.generated = true; await (0, import_generate.generate)({ configPath, useJs, docTools, ...websockets ? { host, port } : {} }); console.log("[StorybookPlugin] Generated storybook.requires"); }); if (liteMode) { const alias = compiler.options.resolve.alias ?? {}; alias["@storybook/react-native-ui$"] = false; compiler.options.resolve.alias = alias; } } /** * When disabled: redirect all Storybook imports to empty modules, * and replace the config folder index with a stub component. */ applyDisabled(compiler, configPath) { const stubPath = require.resolve("@storybook/react-native/stub"); const normalizedConfigPath = path2.resolve(configPath); new compiler.webpack.NormalModuleReplacementPlugin(/./, (resource) => { const request = resource.request; if (!request) return; if (request.startsWith("@storybook") || request.startsWith("storybook")) { resource.request = stubPath; return; } }).apply(compiler); const alias = compiler.options.resolve.alias ?? {}; alias[normalizedConfigPath] = stubPath; compiler.options.resolve.alias = alias; } }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { StorybookPlugin });