Project scaffolding + Webpack 2 + React + PostCSS — Step by step (2/2)

Hey again. In the previous post we provided some basic configurations of webpack 2, along with a simple folder structure that helps us stay organised, covering the first 3 points below.

  1. Project architecture
  2. Global and local dependencies and basic folder structure
  3. Webpack 2 basic configuration
  4. React
  5. PostCSS with some basic plugins for globals, grid, etc

This one aims to give understanding on how we can configure React + PostCSS, covering the last two bullets above.

If you are reading this, you probably already know these tools. So, without any further delays lets get to business.

4. React

When we speak about react we all tend to think about JSX. In the Webpack domain we think about loaders, because they transform stuff into another stuff. In this case JSX to Javascript. Anyway, we will not use any loader to transform JSX, but we will use Babel to transpile code to ES5 and take advantage of the React preset to deal with the JSX.

4.1 Dependencies

Lets start by adding the dependencies.

// react dependencies
yarn add react react-dom react-router
// html management
yarn add --dev html-webpack-plugin
// babel dependencies
yarn add --dev babel-core babel-loader babel-preset-react babel-preset-latest
// in the project root
touch .babelrc

First lets put add the following in the .babelrc:

{
"presets": [
...
"react"
]
}

So, like any other preset in babel, react preset brings a couple of plugins for dealing jsx transformation and such.

4.2 webpack.config.js

html-webpack-plugin may not be so obvious here. This manages script injection in the html for us.

Lets configure it, in the plugins.js:

const HtmlWebpack = require('html-webpack-plugin');
...
const HtmlWebpackPlugin = new HtmlWebpack({
template: path.join(__dirname, '../../src/index.html'),
filename: 'index.html',
inject: 'body', // inject at the bottom of the body tag
});
module.exports = {
...
HtmlWebpackPlugin: HtmlWebpackPlugin
}

And in the webpack.config.js we have the babel loader:

module.exports = env => {
return {
plugins: [
...
plugins.HtmlWebpackPlugin
],
...
module: {
loaders: [{
test: /\.(js)$/,
exclude: /node_modules/,
loader: 'babel',
query: {
cacheDirectory: true
}
}]
}
}
}

Pretty simple. We just tell to exclude node_modules, because we don’t need the burden of parsing external dependencies.

4.3 webpack-dev-server

Ok, lets configure our webpack dev server because we need to have a development server. In the webpack-dev-server.js below ./tools/build:

/**
* Just for development purposes
*/
const webpack = require('webpack');
const WebpackDevServer = require('webpack-dev-server');
const webpackConfig = require('./webpack.config');
const path = require('path');
const config = {
// dynamic route refreshing
historyApiFallback: {
disableDotRule: true
},
contentBase: path.join(__dirname, '../../src/'),
};
// always dev environment when running webpack dev server
const env = {
dev: process.env.NODE_ENV
};
// create the server
const server = new WebpackDevServer(webpack(webpackConfig(env)), config);
// browse at http://localhost:3000/
server.listen(3000, 'localhost');

The code above is pretty much self explanatory. We are creating a web server taking into account the configuration from webpack and the environment. Did you notice the module.exports in webpack.config.js? It can be used to pass the environment as a parameter.

Since we are with our hands dirty, lets go and change our package.json, like so:

...
"scripts": {
"build": "webpack -p --config ./tools/build/webpack.config.js --display-error-details",
"start": "node tools/build/webpack-dev-server"
},
....

display-error-details is just to get more terminal info about the errors that may arise.

4.4 Entrypoints

Did you notice in 4.2 that we told html-webpack-plugin that we had a template called index.html. So, lets add it below too in ./src.

<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
<title>Webpack-2-starter</title>
</head>
<body>
<div id="App"></div>
</body>
</html>

Our app is going to be injected in the div with the id=”App”. Speaking of app, lets change our app.js to include react and company.

'use strict';
import React from 'react';
import { render } from 'react-dom';
render(
<h1>Hello, World!</h1>,
document.getElementById('App')
);

Now, lets give our new scripts a try:

npm run build

The last will build all scripts into one nice little bundle along with the index.html referencing it. The one below will run dev server with the same webpack config file created before.

npm run start

And thats it, we’ve got react working.

5. PostCSS

PostCSS is a tool for transforming styles with JS plugins. These plugins can lint your CSS, support variables and mixins, transpile future CSS syntax, inline images, and more.

The beauty of it is that you can write your own Plugin to do whatever ticks you. For example, someone was getting crazy when a fellow colleague didn’t browse the color palette and added precisely a color that closely resembled one that already existed. Yes, that’s right, someone wrote a plugin called css-colorguard to do just that; find similar colours.

In order to get PostCSS up and running, first we need to tell WebPack how to handle css.

yarn add --dev css-loader extract-text-webpack-plugin file-loader postcss postcss-loader postcss-cssnext postcss-import

Imports are now managed by the css-loader.

extract-text-webpack-plugin extracts moves every require("style.css") in entry chunks into a separate css output file

PostCSS is installed along with postcss-loader which allow us to use PostCSS with WebPack.

Next we use try to be future proof in terms of CSS syntax by using postcss-next plugin.

postcss-next is a PostCSS plugin to use tomorrow’s CSS syntax, today.
postcss-import let us inline content.

So, lets get started with the configurations. First lets add a configuration file next to our webpack configuration file named postcss.config.js.

module.exports = {
plugins: {
'postcss-import': {},
'postcss-cssnext': {
browsers: ['last 2 versions', '> 5%'],
},
},
};

The order up there is important, one should have postcss-import first because other plugins may take the advantage of working on the AST as if there were only a single file to process.

Autoprefixer enables automatic vendor prefixes on the css rules. It uses caniuse in order to enable/disable features.

Since we are adding loaders, lets separate them into a new file for loaders like we did with the plugins, so it can stay a little cleaner. Lets move all the loaders there, like so:

const plugins = require('./plugins');
const BabelLoader = {
test: /\.(js)$/,
exclude: /node_modules/,
loader: 'babel',
query: {
cacheDirectory: true
}
};
const CSSLoader = {
test: /\.css$/,
loader: plugins.ExtractTextPlugin.extract({
loader: 'css-loader?importLoaders=1!postcss-loader'
})
};
module.exports = {
BabelLoader: BabelLoader,
CSSLoader: CSSLoader
}

The resulting webpack.config.js is as follows:

'use strict';
const webpack = require('webpack');
const path = require('path');
const paths = require('./paths');
const plugins = require('./plugins');
const loaders = require('./loaders');
module.exports = env => {
return {
context: path.resolve(__dirname, '../../'),
entry: {
app: paths.APP,
},
output: {
filename: '[name].[hash].js',
path: path.resolve(__dirname, '../../public/dist'),
publicPath: paths.ASSETS // useful if we need a CDN
},
plugins: [
plugins.DefinePlugin,
plugins.HtmlWebpackPlugin
],
module: {
loaders: [
loaders.BabelLoader,
loaders.CSSLoader
]
}
};
};

You can see the result in https://github.com/luisantunesdeveloper/webpack-2-starter

Thats all folks. Thanks for reading.