Search code examples
webpackstorybook

Exporting Individual Components in storybook bundle


So iv'e inherited a small storybook library after a dev resigned, and my webpack/storybook isn't my strongsuit.

The question is how easy would it be to configure a Storybook build to output exportable components. I want to turn my Storybook components (not the stories of the components but the underlying components themselves) into a private npm package so i can export them into other projects.

my storybook is @storybook react 3.2.11 running typescript / scss the structure of my file's look like this...

src
    components
        Button
             Button.tsx
             button.scss
        Dropdown
        ..etc
    stories
        components
            Buttonstory
                 ButtonStory.tsx
        index.tsx

my config.js looks like this...

import { configure } from '@storybook/react';

function loadStories() {
  require('../src/stories/index.tsx');
  // You can require as many stories as you need.
}

configure(loadStories, module);

and my .storybook webpack.config.js looks like this.

const path = require("path");
const autoprefixer = require("autoprefixer");
const genDefaultConfig = 
require('@storybook/react/dist/server/config/defaults/webpack.config.js');
const stylelint = require('stylelint');
const stylelintPlugin = require("stylelint-webpack-plugin");

module.exports = (baseConfig, env) => {
    const config = genDefaultConfig(baseConfig, env);

    // Reset rules
    config.module.rules = [];

    /**
     * Typescript handling
     * Allows webpack to process ts and tsx files using the typescript compiler.
     */
    config.module.rules.push({
        test: /\.(tsx?)$/,
        enforce: "pre",
        loader: require.resolve("tslint-loader")
    });

    config.module.rules.push({
        test: /\.(tsx?)$/,
        loader: require.resolve('awesome-typescript-loader')
    });
    config.resolve.extensions.push('.ts', '.tsx');

    /**
     * Style handling
     * 
     * There are four loaders for handling the styling import process their hierachy is as follows:
     * 
     * sass-loader -> postcss -> css-loader -> style-loader
     */
    config.module.rules.push({
        test: /\.(scss)$/,
        use: [
            require.resolve('style-loader'),
            {
                loader: require.resolve('css-loader'),
                options: {
                    importLoaders: 2
                }
            },
            {
                loader: require.resolve('postcss-loader'),
                options: {
                    ident: 'postcss',
                    sourceMap: true,
                    plugins: function plugins() {
                        return [
                            require('postcss-flexbugs-fixes'),
                            autoprefixer({
                                browsers: ['>1%', 'last 4 versions', 'Firefox ESR', 'not ie < 9'],
                                flexbox: 'no-2009'
                            })
                        ]
                    }
                }
            },
            {
                loader: require.resolve('sass-loader'),
                options: {
                    sourceMap: true
                }
            },
        ]
    })

    /**
     * Begin storybook defaults
     * These are the storybook defaults that we have not written over. Pretty much everything except
     * styling loader and imports.
     */
    config.module.rules = config.module.rules.concat([
        {
            test: /\.json$/,
            loader: require.resolve('json-loader')
        }, 
        {
            test: /\.(ico|jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2)(\?.*)?$/,
            loader: require.resolve('file-loader'),
            query: {
                name: 'static/media/[name].[hash:8].[ext]'
            }
        }, 
        {
            test: /\.(mp4|webm|wav|mp3|m4a|aac|oga)(\?.*)?$/,
            loader: require.resolve('url-loader'),
            query: {
                limit: 10000,
                name: 'static/media/[name].[hash:8].[ext]'
            }
        }
    ]);

    /**
     * Add stylelint as a plugin, doing this because you need this to run before postCSS and as of the
     * of this writing I couldn't be bothered reconfiguring postCSS to process the SCSS.
     */
    config.plugins.push(new stylelintPlugin({
        files: "**/*.scss",
        emitErrors: false,
        failOnError: false
    }));

    return config;
};

wondering if anyone has set up anything similar


Solution

  • As per requested comment i solved this along time ago, had to learn some webpack but basically the final result is that i now have a storybook -> private npm repository -> other project pipeline. here is how i solved it.

    .storybook .webpack.config.js is this.

    const genDefaultConfig = require('@storybook/react/dist/server/config/defaults/webpack.config.js');
    const productionBuild = require("./webpack.prod");
    const developmentBuild = require("./webpack.dev");
    
    module.exports = (baseConfig, env) => {
        const config = genDefaultConfig(baseConfig, env);
        console.log(env);
        config.module.rules = [];
    
        if(env === "PRODUCTION") return productionBuild(config);
        if(env === "DEVELOPMENT") return developmentBuild(config);
    
        return config;
    };
    

    this checks the development or production build then runs it through the appropriate file. (i'll just show production)

    const ExtractTextPlugin = require("extract-text-webpack-plugin");
    // const DashboardPlugin = require('webpack-dashboard/plugin');
    const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
    const autoprefixer = require("autoprefixer");
    const { TsConfigPathsPlugin } = require('awesome-typescript-loader');
    
    module.exports = function(config) {
        config.entry = {};   // remove this line if you want storybook js files in output.
        config.entry["Components"] = ['./src/main.tsx'];
    
        config.output = {
            filename: 'dist/[name].bundle.js',
            publicPath: '',
            libraryTarget: "commonjs"
        };
    +
        config.module.rules.push({
            test: /\.(tsx?)$/,
            enforce: "pre",
            loader: require.resolve("tslint-loader")
        });
    
        config.plugins.push(new TsConfigPathsPlugin())
    
        config.module.rules.push({
            test: /\.(tsx?)$/,
            loader: require.resolve('awesome-typescript-loader'),
            options: {
                configFileName: 'tsconfig.prod.json'
            }
        });
    
        config.module.rules.push({
            test: /\.(scss)$/,
            use: ExtractTextPlugin.extract({
                fallback: 'style-loader',
                use: [
                    {
                        loader: 'css-loader',
                        options: {
                            url: false,
                            minimize: true,
                            sourceMap: false
                        }
                    },
                    {
                        loader: require.resolve('postcss-loader'),
                        options: {
                            ident: 'postcss',
                            sourceMap: false,
                            plugins: function plugins() {
                                return [
                                    require('postcss-flexbugs-fixes'),
                                    autoprefixer({
                                        browsers: ['>1%', 'last 4 versions', 'Firefox ESR', 'not ie < 9'],
                                        flexbox: 'no-2009'
                                    })
                                ]
                            }
                        }
                    },
                    {
                        loader: require.resolve('sass-loader'),
                        options: {
                            sourceMap: false
                        }
                    },
                ]
            })
        });
    
        config.module.rules = config.module.rules.concat([
            {
                test: /\.json$/,
                loader: require.resolve('json-loader')
            },
            {
                test: /\.(ico|jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2)(\?.*)?$/,
                loader: require.resolve('file-loader'),
                query: {
                    name: 'static/media/[name].[hash:8].[ext]'
                }
            },
            {
                test: /\.(mp4|webm|wav|mp3|m4a|aac|oga)(\?.*)?$/,
                loader: require.resolve('url-loader'),
                query: {
                    limit: 10000,
                    name: 'static/media/[name].[hash:8].[ext]'
                }
            }
        ]);
    
        config.resolve.extensions.push('.ts', '.tsx');
    
        config.externals = {
            react: {
                root: 'React',
                commonjs2: 'react',
                commonjs: 'react',
                amd: 'react'
            },
            'react-dom': {
                root: 'ReactDOM',
                commonjs2: 'react-dom',
                commonjs: 'react-dom',
                amd: 'react-dom'
            }
        };
    
        config.plugins.push(new UglifyJSPlugin());
    
        config.plugins.push(new ExtractTextPlugin({
            filename: '[name].css',
            disable: false,
            allChunks: true
        }));
    
    
        /* unneccessary logic below is to stop webpack dashboard stalling the production build  */
        // config.plugins.push({
        //      apply: function() {
        //          const dashboard = new DashboardPlugin({color: "cyan"});
        //          dashboard.apply.apply(dashboard, arguments)
        //      }
        //  }
        // );
    
        return config;
    };
    

    ^ the above code is .storybook -> webpack.prod.js Basically what it does is overrides the entry points to webpack that storybook uses as its default, if you want the storybook assets i would suggest you push to config.entry instead of overriding it.

    What webpack does is traverses a tree of dependencies so Main.tsx is a seperate file containing only the components i want to export so my bundle doesn't get any bloat.

    Finally there are a few small optimiziations to reduce bundle size (1) - Tree Shaking to remove unusued exports (2) - ExtractTextPlugin to get a seperate CSS file (3) - config.externals React and ReactDOM this means that the output bundle will not contain React or ReactDOM and instead it will look for those libraries in the project you IMPORT INTO<<<< (IMPORTANT)

    Below is Main.tsx (webpack entry point)

    export { BEMHelper } from "./utils/bem-helper/bem-helper";
    export { Button } from "./components/Button/Button";
    export { Checkbox } from "./components/Checkbox/Checkbox";
    ...etc etc etc repeat for every component you want to export in your bundle
    import "./styles/bootstrap.scss";
    

    and finally if your using commonJS in your typescript config this will be your index.js (what you need for your NPM package)

    export const BEMHelper = require("./storybook-static/dist/Components.bundle").BEMHelper;
    export const Button = require('./storybook-static/dist/Components.bundle').Button;
    ..... etc etc etc. (repeat)