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.
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"
},
...
}
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"
},
...
}
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"
}
}
}
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
package.json
To build before publishing.
"scripts": {
"prepublish": "rm -rf ./dist && npm run build",
...
}
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'
]
})
},
....
]
}
}
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');
}
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')
}
}
]
}
}
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"
}
}
};
February 26, 2018