I am in the process of developing a design system using React Native Web and React Native as an npm package. While I've made progress in getting all the expo-vector-icon related components to work on Storybook when running on the web, I've encountered some difficulties when building expo-vector-icons using Rollup. Despite spending numerous hours debugging and even seeking assistance from chat GPT, I couldn't resolve the issues. Additionally, when I bundled expo-vector-icons into my app, it still didn't function as expected.
As a workaround, I decided to remove expo-vector-icons entirely from my package. Instead, I've been passing down an "IconToUse" prop to render icons, and I create this component locally within my Expo app or wherever I need it. However, this approach is not ideal because I need it to be testable in Storybook on the web, primarily for presentation purposes during meetings.
It's been a few months since my last attempt, and I'm reaching out to the Stack Overflow community for guidance. If anyone can provide insights or share an example Rollup configuration that works seamlessly with React Native, React Native Web, and Storybook for the web, I would greatly appreciate it. I'm open to using either react-native-vector-icons directly or continuing to use expo-vector-icons. My primary requirements are that it compiles into CommonJS (cjs), Universal Module Definition (umd), and ES Module (esm) formats and can be easily integrated into multiple projects.
Current Rollup Configuration:
import babel from "@rollup/plugin-babel";
import commonjs from "@rollup/plugin-commonjs";
import inject from "@rollup/plugin-inject";
import json from "@rollup/plugin-json";
import { nodeResolve } from "@rollup/plugin-node-resolve";
import typescript from "@rollup/plugin-typescript";
import url from '@rollup/plugin-url';
import svgr from '@svgr/rollup';
import fs from "fs";
import peerDepsExternal from 'rollup-plugin-peer-deps-external';
import nodePolyfills from "rollup-plugin-polyfill-node";
import postcss from 'rollup-plugin-postcss';
const packageJson = JSON.parse(fs.readFileSync("./package.json", "utf-8"));
const extensions = [".js", ".jsx", ".ts", ".tsx", ".native.js"];
// List any external dependencies here, including peer dependencies and Storybook-related packages if any.
const externals = [
"react",
"react-dom",
"react-native",
"react-native-svg",
"styled-components",
"styled-components/native",
"expo-linear-gradient",
"expo-checkbox",
];
// Define globals for UMD build, ensure no Storybook globals are present.
const globals = {
'react': 'React',
'react-dom': 'ReactDOM',
"react-native-svg": "ReactNativeSvg",
};
const makeExternalPredicate = externalArr => {
if (externalArr.length === 0) {
return () => false;
}
const pattern = new RegExp(`^(${externalArr.join("|")})($|/)`);
return id => pattern.test(id);
};
export default {
plugins: [
peerDepsExternal(),
nodeResolve({
extensions,
preferBuiltins: true,
mainFields: ['module', 'main', 'browser'],
dedupe: ['react', 'react-dom', 'react-native'],
}),
commonjs({
include: /node_modules/,
extensions,
}),
babel({
extensions,
babelHelpers: "bundled",
exclude: /node_modules|.*\.stories\.tsx?|.*\.story\.tsx?/,
presets: [
"@babel/preset-env",
"@babel/preset-react",
"@babel/preset-typescript",
],
}),
typescript(),
nodePolyfills(),
json(),
svgr(),
postcss(),
url(),
inject({
Svg: ['react-native-svg', 'default'],
Circle: ['react-native-svg', 'Circle'],
Checkbox: ['expo-checkbox', 'default'],
Platform: ['react-native', 'Platform'],
View: ['react-native', 'View'],
Text: ['react-native', 'Text'],
Image: ['react-native', 'Image'],
StyleSheet: ['react-native', 'StyleSheet'],
TouchableOpacity: ['react-native', 'TouchableOpacity'],
Platform: ['react-native', 'Platform'],
Dimensions: ['react-native', 'Dimensions'],
StatusBar: ['react-native', 'StatusBar'],
// Inject 'react-native-vector-icons' imports
Icon: ['react-native-vector-icons/Ionicons', 'default'],
}),
],
input: "src/index.tsx",
external: makeExternalPredicate(externals),
output: [
{
file: packageJson.main,
format: "cjs",
name: "DesignSystem",
sourcemap: true,
globals,
},
{
file: packageJson.browser,
format: "umd",
name: "DesignSystem",
sourcemap: true,
globals,
},
{
file: packageJson.module,
format: "esm",
sourcemap: true,
globals,
},
],
};
import type { StorybookConfig } from "@storybook/react-webpack5";
import path from "path";
import webpack from "webpack";
const config: StorybookConfig = {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
addons: [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-interactions",
"@storybook/addon-jest",
"@storybook/addon-designs",
"@storybook/addon-viewport",
{
name: "@storybook/addon-react-native-web",
options: {
modulesToTranspile: [
"react-native",
"react-native-svg",
"expo-image",
"expo-asset",
"react-native/Libraries/Image/AssetRegistry",
],
},
},
"@storybook/addon-webpack5-compiler-babel",
"@chromatic-com/storybook",
],
webpackFinal: async (config) => {
if (!config.resolve) {
config.resolve = {};
}
if (!config.resolve.alias) {
config.resolve.alias = {};
}
// Alias 'react-native' to 'react-native-web'
config.resolve.alias["react-native$"] = "react-native-web";
// Add alias for 'react-dom/client' to ensure React 17 compatibility
config.resolve.alias["react-dom/client"] = require.resolve("react-dom");
// Exclude 'child_process' from bundling
config.externals = {
child_process: "child_process",
};
if (!config.plugins) {
config.plugins = [];
}
config.plugins.push(
new webpack.NormalModuleReplacementPlugin(
/react-native-svg\/lib\/module\/fabric/,
"react-native-svg-web"
)
);
config.plugins.push(
new webpack.NormalModuleReplacementPlugin(
/react-native\/Libraries\/Image\/AssetRegistry/,
path.resolve(__dirname, "./mocks/AssetRegistry.ts")
)
);
if (!config.resolve.fallback) {
config.resolve.fallback = {};
}
config.resolve.fallback = {
...config.resolve.fallback,
os: require.resolve("os-browserify/browser"),
tty: require.resolve("tty-browserify"),
};
if (config.module && config.module.rules) {
// Include the necessary loaders for React Native modules
config.module.rules.push({
test: /\.(js|ts|tsx)$/,
include: /node_modules\/(react-native|expo|@react-native|react-native-web|expo-image|expo-asset)/,
use: {
loader: "babel-loader",
options: {
presets: ["module:metro-react-native-babel-preset", "@babel/preset-typescript"],
},
},
});
}
return config;
},
framework: {
name: "@storybook/react-webpack5",
options: {
legacyRootApi: true, // Required for React 17
},
},
typescript: {
reactDocgen: "react-docgen-typescript",
},
};
export default config;