@storybook/react-native
Version:
A better way to develop React Native Components for your app
615 lines (597 loc) • 21.1 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 __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
});