Category: React


How to package your React Component for distribution via NPM

I wrote a React component, transpiling using Babel, bundling and building using Webpack. I wanted to use it in another application via NPM. My NPM publish package needed to include component behavior, styles and images. So how difficult is it to package my React Component for distribution via NPM? An hour or so of work maybe right?

Well, it took me a lot longer to figure this out because of the following speed-bumps.
– React version conflict
– How to bundle and consume styles from a component
– How to include and bundle images

Here are the steps.

1. Make a package npm publishable

npm init

In the package.json, make sure these fields are populated:

package.json
{
    "name": "myUnflappableComponent",
    "version": "0.0.29",
    "main": "dist/index.js",
    "publishConfig": {
       "access": "restricted"
    },
    ...
}

2. Don’t bundle React. Use the parent’s React and react-dom.

  1. In package.json, add React and react-dom in the project’s peerDependencies (And remove it from dependencies, but add it to devDependencies for development)
    {
     ...
     "peerDependencies": {
         "react": ">=15.0.1",
         "react-dom": ">=15.0.1"
     },
     "devDependencies": {
         "react": ">=15.0.1",
         "react-dom": ">=15.0.1"
     },
     ...
    }
    
  2. In your webpack configuration, create a UMD bundle
     ...
     module.exports = {
     ...
     output: {
         path: path.join(__dirname, './dist'),
         filename: 'myUnflappableComponent.js',
         library: libraryName,
         libraryTarget: 'umd',
         publicPath: '/dist/',
         umdNamedDefine: true
     },
     plugins: {...},
     module: {...},
     resolve: {...},
     externals: {...}
    }
    

    And super-duper important, don’t bundle React

     module.exports = {
     output: {...},
     plugins: {...},
     module: {...},
     resolve: {
         alias: {
             'react': path.resolve(__dirname, './node_modules/react') ,
             'react-dom': path.resolve(__dirname, './node_modules/react-dom'),
         }
     },
     externals: {
         // Don't bundle react or react-dom
         react: {
             commonjs: "react",
             commonjs2: "react",
             amd: "React",
             root: "React"
         },
         "react-dom": {
             commonjs: "react-dom",
             commonjs2: "react-dom",
             amd: "ReactDOM",
             root: "ReactDOM"
         }
     }
    }
    

3. Set up your .npmignore file

If you don’t set up a .npmignore file, npm uses your .gitignore file and bad things will happen. An empty .npmignore file is allowed. This is what mine looks like:

    webpack.local.config.js
    webpack.production.config.js
    .eslintrc
    .gitignore

4. Add a ‘prepublish’ script to your package.json

To build before publishing.

"scripts": {
     "prepublish": "rm -rf ./dist && npm run build",
    ...
}

5. Extract out your CSS files for use

We use SCSS files for our styles. These are compiled into css and extracted out by Webpack.

Install the following:

npm install --save-dev extract-text-webpack-plugin node-sass style-loader css-loader sass-loader

Update your webpack.config

const ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
    ...
    plugins:[
        new ExtractTextPlugin({
            filename: 'myUnflappableComponent.css',
        }),
    ],
    module:{
        rules:[
            {
                test: /\.*css$/,
                use : ExtractTextPlugin.extract({
                    fallback : 'style-loader',
                    use : [
                        'css-loader',
                        'sass-loader'
                    ]
                })
            },
            ....
        ]
    }
}

6. Images in CSS

The way you include images in your component will determine if the consumer of your component will get them.

I include them in the css file using the content property. For example

.mySky{
    width: 20px;
    height: 20px;
    content: url('../assets/images/thunderSky.png');
}

7. Make sure your images are available outside your component

The issue I ran into was the relative paths of the images in the published CSS files were messed up. After a lot of searching, this article (also in the links below) helped.

Install the following:

npm install --save-dev file-loader url-loader

Update your webpack.config like this:

module.exports = {
    ...
    module: {
        rules: [
            {
                test: /\.(png|svg|jpg|gif)$/,
                use: [
                    {
                        loader: 'url-loader',
                        options:{
                            fallback: "file-loader",
                            name: "[name][md5:hash].[ext]",
                            outputPath: 'assets/',
                            publicPath: '/assets/'
                        }
                    }
                ]
            },
            ...
            resolve: {
                alias:{
                    ...
                    'assets': path.resolve(__dirname, 'assets')
                }
            }
        ]
    }
}

Shoutouts and References:

  1. How to publish your package on npm(all about package.json)

  2. Publish Beta to NPM

  3. Exporting images via webpack (webpack.config.js)

  4. My full webpack configuration:

const webpack = require('webpack');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const pkg = require('./package.json');
const path = require('path');

const libraryName= pkg.name;

module.exports = {
    entry: path.join(__dirname, "./src/index.js"),
    output: {
        path: path.join(__dirname, './dist'),
        filename: 'myUnflappableComponent.js',
        library: libraryName,
        libraryTarget: 'umd',
        publicPath: '/dist/',
        umdNamedDefine: true
    },
    plugins: [
        new ExtractTextPlugin({
            filename: 'myUnflappableComponent.css',
        }),
    ],
    node: {
      net: 'empty',
      tls: 'empty',
      dns: 'empty'
    },
    module: {
        rules : [
            {
            test: /\.(png|svg|jpg|gif)$/,
            use: [
                {
                    loader: 'url-loader',
                    options:{
                        fallback: "file-loader",
                        name: "[name][md5:hash].[ext]",
                        outputPath: 'assets/',
                        publicPath: '/assets/'
                    }
                }    
            ]
        },
        {
            test: /\.*css$/,
            use : ExtractTextPlugin.extract({
                fallback : 'style-loader',
                use : [
                    'css-loader',
                    'sass-loader'
                ]
            })
        },
        {
            test: /\.(js|jsx)$/,
            use: ["babel-loader"],
            include: path.resolve(__dirname, "src"),
            exclude: /node_modules/,
        },
        {
            test: /\.(eot|ttf|woff|woff2)$/,
            use: ["file-loader"],
        },
        {
            test: /\.(pdf|doc|zip)$/,
            use: ["file-loader"],
        }]
    },
    resolve: { 
        alias: { 
            'react': path.resolve(__dirname, './node_modules/react') ,
            'react-dom': path.resolve(__dirname, './node_modules/react-dom'),
            'assets': path.resolve(__dirname, 'assets')
        } 
    },
    externals: {
        // Don't bundle react or react-dom
        react: {
            commonjs: "react",
            commonjs2: "react",
            amd: "React",
            root: "React"
        },
        "react-dom": {
            commonjs: "react-dom",
            commonjs2: "react-dom",
            amd: "ReactDOM",
            root: "ReactDOM"
        }
    }
};