React

workflows

Without Create-React-App

Second Edition

Created by Maria D. Campbell / @letsbsocial1

About This Presentation

This is the second edition of my React Workflows Without CRA. If you want to acquaint yourself with the first edition as well, please visit the React Workflow Presentation repository. For a detailed map of this edition, please visit the README of this edition's repository. To view the source code of this workflow, please visit the speech-to-text-app repository.

Hi There!

I'm Maria.

About Me

I've always been obsessed with development and construction in one way or another.

Background

  • Fashion design and manufacture.
  • Graphics/production art.
  • Now Fullstack JS, with a focus on React.

?

So why am I here today to discuss React workflows?

Simple.

When I started learning React, I was not happy with the fact that virtually all React learning seemed to revolve around using create-react-app to create React applications.

Horse blinders

And that meant blindly accepting its extensive boiler-plate.

Why blindly ? Because the workflow is hidden by default. In order to see what is going on, you have to eject out of CRA.

But once you eject, THERE IS NO GOING BACK!

And one thing I know for sure ...

Starting Over

Sometimes it's faster (and easier) to start from scratch than try to manipulate to your needs.

And that's where

webpack

(4 and beyond) come in.

?

So why webpack ?

Grunt vs Gulp

If done from scratch, it can mean fewer workflow files and lines of code. If you come from task runners like Gulp (I do) or Grunt, you know what I'm talking about.

Create-React-App

And if you are familiar with CRA, you know how extensive its boilerplate is.

Nothing wrong with that

if your focus is on writing JSX and JS for the browser.

The Problem

with that approach is that when developers start heavily relying on tools like that, they can lose site of the bigger picture.

  • Of how things work.
  • Why they work as they do.
  • What their options are.

Which can STUNT one's growth as a developer.

🐜

I first got the bug to create my own custom React workflows when I created my first React application using CRA.

Debugging Difficulties

I would get errors in my React projects which I sometimes found difficult to debug. They might or might not have been CRA related.

Custom React Workflows

So I decided to start creating my own React workflows using webpack.

What This React Workflow Supports

  • Eslint Configuration for React
  • Jest Testing
  • Bundle Splitting using the SplitChunksPlugin
  • Code Splitting using the SplitChunksPlugin and dynamic imports
  • CSS Modules
  • .SCSS
  • Image/File Import

What This React Workflow Uses

  • React 16.6.3
  • Webpack 4.3.0
  • Babel 7
  • ESLint 5.9.0
  • Jest 23.6.0

package.json


                {
                    "name": "speech-to-text-app",
                    "version": "0.0.1",
                    "private": true,
                    "description": "\"A voice controlled notes app that allows you to take notes by recording your voice\"",
                    "main": "index.js",
                    "scripts": {
                        "test": "jest",
                        "lint": "eslint .",
                        "clean": "rimraf dist",
                        "cleanSrc": "rimraf dist/src",
                        "start": "webpack-dev-server --mode development --config config/webpack.base.config.js --open --hot
                        --history-api-fallback --env.PLATFORM=local --env.VERSION=stag",
                        "predeploy": "webpack --mode production --config config/webpack.prod.config.js --env.PLATFORM=production
                        --env.VERSION=stag --progress",
                        "deploy": "gh-pages -d dist"
                    },
                    "jest": {
                        "setupFiles": [
                            "raf/polyfill"
                        ],
                        "moduleNameMapper": {
                            "\\.(pdf|jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/__mocks__/fileMock.js",
                            "\\.(css|less|scss)$": "identity-obj-proxy"
                        }
                    },
                    "author": "Maria D. Campbell",
                    "license": "ISC",
                    "dependencies": {
                        "@fortawesome/fontawesome-svg-core": "^1.2.8",
                        "@fortawesome/free-brands-svg-icons": "^5.5.0",
                        "@fortawesome/free-regular-svg-icons": "^5.5.0",
                        "@fortawesome/free-solid-svg-icons": "^5.5.0",
                        "@fortawesome/react-fontawesome": "^0.1.3",
                        "core-js": "^2.5.7",
                        "gh-pages": "^2.0.1",
                        "react": "^16.6.3",
                        "react-dom": "^16.6.3"
                    },
                    "devDependencies": {
                        "@babel/core": "^7.1.6",
                        "@babel/plugin-proposal-class-properties": "^7.1.0",
                        "@babel/plugin-proposal-export-namespace-from": "^7.0.0",
                        "@babel/plugin-proposal-throw-expressions": "^7.0.0",
                        "@babel/plugin-syntax-dynamic-import": "^7.0.0",
                        "@babel/polyfill": "^7.0.0",
                        "@babel/preset-env": "^7.1.5",
                        "@babel/preset-react": "^7.0.0",
                        "@babel/register": "^7.0.0",
                        "autoprefixer": "^9.3.1",
                        "babel-core": "^7.0.0-bridge.0",
                        "babel-eslint": "^10.0.1",
                        "babel-jest": "^23.6.0",
                        "babel-loader": "^8.0.4",
                        "clean-webpack-plugin": "^1.0.0",
                        "copy-webpack-plugin": "^4.6.0",
                        "css-loader": "^1.0.1",
                        "enzyme": "^3.7.0",
                        "eslint": "^5.9.0",
                        "eslint-config-airbnb": "^17.1.0",
                        "eslint-loader": "^2.1.1",
                        "eslint-plugin-import": "^2.14.0",
                        "eslint-plugin-jsx-a11y": "^6.1.2",
                        "eslint-plugin-react": "^7.11.1",
                        "file-loader": "^2.0.0",
                        "html-webpack-plugin": "^3.2.0",
                        "identity-obj-proxy": "^3.0.0",
                        "jest": "^23.6.0",
                        "jest-enzyme": "^7.0.1",
                        "mini-css-extract-plugin": "^0.4.4",
                        "node-sass": "^4.10.0",
                        "optimize-css-assets-webpack-plugin": "^5.0.1",
                        "postcss": "^7.0.5",
                        "postcss-loader": "^3.0.0",
                        "prop-types": "^15.6.2",
                        "raf": "^3.4.1",
                        "react-test-renderer": "^16.6.1",
                        "regenerator-runtime": "^0.12.1",
                        "rimraf": "^2.6.2",
                        "sass-loader": "^7.1.0",
                        "style-loader": "^0.23.1",
                        "uglifyjs-webpack-plugin": "^2.0.1",
                        "url-loader": "^1.1.2",
                        "webpack": "^4.3.0",
                        "webpack-cli": "^3.1.2",
                        "webpack-dev-server": "^3.1.10",
                        "webpack-manifest-plugin": "^2.0.4",
                        "webpack-merge": "^4.1.4",
                        "webpack-visualizer-plugin": "^0.1.11"
                    }
                }
                

Please ignore the closing rootdir tag at the bottom of the package.json.

What Has Not Changed (Jest)


                "jest": {
                    "setupFiles": [
                        "raf/polyfill"
                    ],
                

React 16+ depends on requestAnimationFrame, even in test environments. First install the raf npm package, and then add this to your package.json. This was the same in my previous workflow.

What Has Changed (Jest)


                "moduleNameMapper": {
                    "\\.(pdf|jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/__mocks__/fileMock.js",
                    "\\.(css|less|scss)$": "identity-obj-proxy"
                }
                

Even though all that has changed here is adding |scss to identity-obj-proxy, so much else changed that resulted in the need to add this to the moduleNameMapper property. Again, please ignore the closing rootdir tag.

What Has Not Changed (Jest devDeps)


                "babel-jest": "^23.6.0",
                "enzyme": "^3.7.0",
                "identity-obj-proxy": "^3.0.0",
                "jest": "^23.6.0",
                "jest-enzyme": "^7.0.1",
                "raf",
                "react-test-renderer": "^16.6.1",
                "regenerator-runtime": "^0.12.1",   
                

What Has Changed (Jest DevDeps)


                "@babel/core": "^7.1.6",
                "babel-core": "^7.0.0-bridge.0",
                

These packages, along with babel-jest and regenerator-runtime are what is needed now in order to be able to use Jest with Babel 7. To learn more about using Babel with Jest, please visit Using Babel.

Configuring Babel 7

everything changed

Configuring Babel 7

.babelrc


                {
                    "presets": ["env", "react", "stage-1", "stage-2", "jest"]
                }
                

Previously, the babel config looked something like this.

Configuring Babel 7

.babelrc


                {
                    "presets": [
                        "@babel/preset-env",
                        "@babel/preset-react"
                    ],
                    "plugins": [
                        "@babel/plugin-syntax-dynamic-import",
                        "@babel/plugin-proposal-class-properties",
                        "@babel/plugin-proposal-export-namespace-from",
                        "@babel/plugin-proposal-throw-expressions"
                    ]
                }
                

Now, the babel config looks something like this.

Babel < 7 Presets

Before Babel 7, proposal plugins were clumped together within their appropriate presets. i.e., stage-0, stage-1, stage-2, etc.

Babel 7 Presets

.babelrc


                    "presets": [
                        "@babel/preset-env",
                        "@babel/preset-react"
                    ],
                

With Babel 7, presets were removed from the webpack config and only appear in the .babelrc file. Only presets which were actually approved appeared in the presets array.

Babel 7 Presets


                    "@babel/preset-env"
                

"@babel/preset-env" is the Babel preset which allows you to use the latest JS.

Babel 7 Presets


                    "@babel/preset-react"
                

"@babel/preset-react" is the Babel preset which allows you to use React code.

Babel 7 Presets

naming

The naming of presets changed with Babel 7 to differentiate between the versions and maintain naming consistency throughout. Instead of simply adding "env" or "react" to the preset array, you now have to add "@babel/preset-env" or "@babel/preset-react", for example.

Babel 7 Plugins

the whys


                    "@babel/plugin-proposal-class-properties": "^7.1.0",
                

Transforms ES6 class syntax into ES5 constructor functions. But the class syntax feature has not yet been approved, so it is still called a proposal. With Babel 7, we must add the equivalent Babel plugin for each modern JS feature we want to implement in our applications. That's because there are no longer stage presets in Babel 7.

Babel 7 Plugins

the whys


                    "@babel/plugin-proposal-export-namespace-from": "^7.0.0",
                

ES6 modules allow us to import something from something else:


                    import * as name from "module-name";
                

Babel 7 Plugins

the whys


                    "@babel/plugin-proposal-export-namespace-from"
                

However, there was no corresponding export!

Babel 7 Plugins

the whys contd


                    "@babel/plugin-proposal-export-namespace-from"
                

plugin proposal: export namespace from


                    export * as ns from "mod";
                

Babel 7 Plugins

the whys contd


                    "@babel/plugin-proposal-throw-expressions": "^7.0.0",
                

example:


                    function test(param = throw new Error('required!')) {
                        const test = param === true || throw new Error('Falsey!');
                    }
                

Babel 7 Plugins

the whys contd


                    "@babel/plugin-syntax-dynamic-import": "^7.0.0",
                

import() returns a promise, so it can be used with async functions. However, doing this requires the "@babel/plugin-syntax-dynamic-import" plugin in Webpack.

Configuring Babel 7

config/webpack.base.config.js


                rules: [{
                    test: /\.js$/,
                    exclude: /node_modules/,
                    use: {
                        loader: 'babel-loader'
                    }
                },
                

Now .JS$ rules looks like this. No more presets. Only in .babelrc. In addition, I changed the name of the webpack config file. I will talk about that later.

JS$ test property


            	test: /\.js$/,
            	

The test property tells webpack what kind of file to match to the babel-loader. The regex /\.js$/ refers to any extension that matches .js at the end of a filename.

Exclude Property


                exclude: /node_modules/,
                

The exclude property means that whatever modules match the /node_modules/ regex should be excluded from bundle.js.

JS$ use property


                use: {
                    loader: 'babel-loader',
                },
            	

Then it uses that loader to transform those files before adding it to bundle.js.

Babel 7 Packages

the whys


                @babel/core
                

This is the Babel core compiler. It makes it possible to use babel-loader. Most babel plugins/top level packages have a peerDependency on @babel/core. Previously it had been babel-core. It is also needed for Jest. To learn more about peerDependencies, click here.

Babel 7 Packages

the whys


                "babel-core": "^7.0.0-bridge.0"
                

This package is needed for Jest testing if Babel 7 is being used. This package is meant to ease the transition libraries that use "babel-core" as a peerDependency for Babel 6. To learn more, please visit the babel-bridge repo .

Babel 7 Packages

the whys


                "@babel/polyfill": "^7.0.0"
                

React 16+ depends on collection types Map and Set. @babel/polyfill emulates a full ES2015+ environment and is for applications. You can use built-ins like Promise or WeakMap, static methods like Array.from or Object.assign, instance methods like Array.prototype.includes, and generator functions provided you use the regenerator plugin.

Babel 7 Packages

the whys


                "@babel/register": "^7.0.0",
                

Allows you to use the require module in your applications.

Configuring ESLint

package.json


                "babel-eslint": "^10.0.1",
                "eslint": "^5.9.0",
                "eslint-loader": "^2.1.1",
                "eslint-plugin-import": "^2.14.0",
                "eslint-plugin-react": "^7.11.1",
                

Configuring ESLint

.eslintrc.json


                {
                    "parser": "babel-eslint",
                    "env": {
                        "browser": true,
                        "commonjs": true,
                        "es6": true,
                        "node": true,
                        "jest": true
                    },
                    "extends": ["eslint:recommended", "plugin:react/recommended", "plugin:import/recommended"],
                    "settings": {
                        "react": {
                            "pragma": "React",
                            "version": "16.6.3"
                        }
                    },
                    "parserOptions": {
                        "ecmaFeatures": {
                            "jsx": true
                        },
                        "ecmaVersion": 2018,
                        "sourceType": "module"
                    },
                    "plugins": [
                        "import",
                        "react"
                    ],
                    "rules": {
                        "no-unused-vars": 0,
                        "no-console": 0,
                        "max-len": [1, 120, 2, {
                            "ignoreComments": true
                        }],
                        "prop-types": [0],
                        "indent": [
                            0
                        ],
                        "linebreak-style": [
                            "error",
                            "unix"
                        ],
                        "quotes": [
                            0
                        ],
                        "semi": [
                            0
                        ]
                    }
                }
                

Configuring ESLint

config/webpack.base.config.js


                {
                    test: /\.js$/,
                    exclude: /node_modules/,
                    loaders: [
                        'babel-loader',
                        'eslint-loader'
                    ]
                },
                

For my complete write up on configuring ESLint with React, please visit The Importance Of ESlint (And React) . To view the complete webpack.base.config.js file, click here .

Configuring Webpack

much changed

Configuring Webpack

I still have two webpack config files, but now they reside in a config folder and are also named differently.

One file is called webpack.base.config.js and is for webpack configs related to the development environment, some of which also are related to production. The other is webpack.prod.config.js and is for webpack configs related only to the production environment.

Configuring Webpack

why the new configs

Setting up Webpack with these two new configs means drier code. No duplications as before. Click here to visit a repo containing my previous workflow.

Configuring Webpack

what requires/variables have NOT changed (base config)


                const path = require('path');
                const webpack = require('webpack');
                

As you can see, not much!

Configuring Webpack

what requires/variables have changed (base config)


                const CopyWebpackPlugin = require('copy-webpack-plugin');
                const merge = require('webpack-merge');
                const MiniCssExtractPlugin = require('mini-css-extract-plugin');
                const HtmlWebpackPlugin = require('html-webpack-plugin');
                const CleanWebpackPlugin = require('clean-webpack-plugin');
                const WorkboxPlugin = require('workbox-webpack-plugin');
                

Configuring Webpack

how module.exports has changed (base config)


                module.exports = env => {

                    const {PLATFORM, VERSION} = env;

                        return merge([
                            {
                                entry: {
                                    main: './src/index.js'
                                },
                                output: {
                                    filename: PLATFORM === 'production' ? 'scripts/[name].[contenthash].chunk.js' : 'scripts/[name].chunk.js',
                                    chunkFilename: PLATFORM === 'production' ? 'scripts/[name].[contenthash].chunk.js' : 'scripts/[name].chunk.js',
                                    path: path.resolve(__dirname, '../dist')
                                },
                                optimization: {
                                    splitChunks: {
                                        chunks: 'all'
                                    }
                                },
                                module: {
                                    rules: [{
                                        test: /\.js$/,
                                        exclude: /node_modules/,
                                        use: {
                                            loader: 'babel-loader'
                                        }
                                    },
                                    {
                                        test: /\.js$/,
                                        exclude: /node_modules/,
                                        loaders: [
                                            'babel-loader',
                                            'eslint-loader'
                                        ]
                                    },
                                    {
                                        test: /\.(scss|sass|css)$/,
                                        exclude: /node_modules/,
                                        loaders: [
                                            PLATFORM === 'production' ? MiniCssExtractPlugin.loader : 'style-loader',
                                            {
                                                loader: 'css-loader',
                                                options: {
                                                    modules: true,
                                                    sourceMap: true,
                                                    importLoaders: 1,
                                                    localIdentName: '[local]___[hash:base64:5]'
                                                },
                                            },
                                            'postcss-loader',
                                            'sass-loader'
                                        ]
                                    },
                                    {
                                        test: /\.(jpg|png|gif|svg|pdf|ico)$/,
                                            use: [{
                                                loader: 'file-loader',
                                                options: {
                                                    name: '[path][name].[hash:8].[ext]'
                                                },
                                            }],
                                    },
                                ]
                            },
                            plugins: [
                                new CleanWebpackPlugin(['dist']),
                                new HtmlWebpackPlugin({
                                    template: 'src/index.html',
                                    favicon: 'src/favicon.ico', /* remove */
                                    styles: 'src/styles.css',
                                    inject: true
                                }),
                                new MiniCssExtractPlugin({
                                    filename: PLATFORM === 'production' ? 'styles/[name].[hash].css' : '[name].css',
                                }),
                                new CopyWebpackPlugin([{
                                    from: 'src/static'
                                }]),
                                new WorkboxPlugin.GenerateSW({
                                    swDest: 'sw.js'
                                }),
                                new webpack.DefinePlugin({
                                    'process.env.VERSION': JSON.stringify(env.VERSION),
                                    'process.env.PLATFORM': JSON.stringify(env.PLATFORM)
                                })
                            ],
                        },
                    ])
                }
                

Configuring Webpack

config/webpack.base.config.js


                const path = require('path');
                const webpack = require('webpack');
                const CopyWebpackPlugin = require('copy-webpack-plugin');
                const merge = require('webpack-merge');
                const MiniCssExtractPlugin = require('mini-css-extract-plugin');
                const HtmlWebpackPlugin = require('html-webpack-plugin');
                const CleanWebpackPlugin = require('clean-webpack-plugin');
                const WorkboxPlugin = require('workbox-webpack-plugin');

                module.exports = env => {

                    const {PLATFORM, VERSION} = env;

                        return merge([
                            {
                                entry: {
                                    main: './src/index.js'
                                },
                                output: {
                                    filename: PLATFORM === 'production' ? 'scripts/[name].[contenthash].chunk.js' : 'scripts/[name].chunk.js',
                                    chunkFilename: PLATFORM === 'production' ? 'scripts/[name].[contenthash].chunk.js' : 'scripts/[name].chunk.js',
                                    path: path.resolve(__dirname, '../dist')
                                },
                                optimization: {
                                    splitChunks: {
                                        chunks: 'all'
                                    }
                                },
                                module: {
                                    rules: [{
                                            test: /\.js$/,
                                            exclude: /node_modules/,
                                            use: {
                                                loader: 'babel-loader'
                                            }
                                        },
                                        {
                                            test: /\.js$/,
                                            exclude: /node_modules/,
                                            loaders: [
                                                'babel-loader',
                                                'eslint-loader'
                                            ]
                                        },
                                        {
                                            test: /\.(scss|sass|css)$/,
                                            exclude: /node_modules/,
                                            loaders: [
                                                PLATFORM === 'production' ? MiniCssExtractPlugin.loader : 'style-loader',
                                                {
                                                    loader: 'css-loader',
                                                    options: {
                                                        modules: true,
                                                        sourceMap: true,
                                                        importLoaders: 1,
                                                        localIdentName: '[local]___[hash:base64:5]'
                                                    },
                                                },
                                                'postcss-loader',
                                                'sass-loader'
                                            ]
                                        },
                                        {
                                            test: /\.(jpg|png|gif|svg|pdf|ico)$/,
                                                use: [{
                                                    loader: 'file-loader',
                                                    options: {
                                                        name: '[path][name].[hash:8].[ext]'
                                                    },
                                                }],
                                        },
                                    ]
                                },
                                plugins: [
                                    new CleanWebpackPlugin(['dist']),
                                    new HtmlWebpackPlugin({
                                        template: 'src/index.html',
                                        favicon: 'src/favicon.ico', /* remove */
                                        styles: 'src/styles.css',
                                        inject: true
                                    }),
                                    new MiniCssExtractPlugin({
                                        filename: PLATFORM === 'production' ? 'styles/[name].[hash].css' : '[name].css',
                                    }),
                                    new CopyWebpackPlugin([{
                                        from: 'src/static'
                                    }]),
                                    new WorkboxPlugin.GenerateSW({
                                        swDest: 'sw.js'
                                    }),
                                    new webpack.DefinePlugin({
                                        'process.env.VERSION': JSON.stringify(env.VERSION),
                                        'process.env.PLATFORM': JSON.stringify(env.PLATFORM)
                                    })
                                ],
                            },
                        ])
                }
                

Dependency Graph

When webpack processes your app, it recursively builds a dependency graph that includes every module that your app needs, and then packages all of those modules into a small number of bundles to be loaded by the browser.

webpack basics

To get started, you only need to understand 4 basic concepts:

  • Entry
  • Output
  • Loaders
  • Plugins

entry


                module.exports = {
                    entry: {
                        main: './src/index.js'
                    },
                }
                

The starting point of webpack's dependency graph is called the entry point. The entry point tells webpack where to start its bundling process, and then follows the graph of dependencies to know what to bundle.

entry


                module.exports = {
                    entry: {
                        main: './src/index.js'
                    },
                }
                

The entry point did change since the previous workflow, and module.exports, and what now comes before entry did as well.

Configuring Webpack

module.exports changed


                module.exports = env => {
                    const {PLATFORM, VERSION} = env;
                };
                

The env variable is passed to module.exports so that I can make those webpack configurations which are required both in development and production. PLATFORM refers to dev or prod envs, and VERSION refers to staging (stag).

entry changed


                entry: {
                    main: './src/index.js'
                },
                

I no longer include a VENDOR_LIBS (vendor) entry. That means I no longer define a VENDOR_LIBS variable containing the third party dependencies I use in my application. This has to do with the new way of bundle splitting in webpack 4 using the SplitChunksPlugin instead of CommonsChunkPlugin.

webpack-merge

webpack-merge is what makes it possible to merge my two configs together into one, thereby resulting in much drier code as well.

main


                module.exports = env => {

                    const {PLATFORM, VERSION} = env;

                        return merge([
                            {
                                entry: {
                                    main: './src/index.js'
                            },
                        ])
                }
                

The value of the main property indicates to webpack what code needs to be bundled. Previously I called it bundle, but now for consistency and readability sake, I changed it to main.

main index.js


                import React from 'react';
                import ReactDOM from 'react-dom';
                import 'core-js/es6/map';
                import 'core-js/es6/set';
                import App from './App';
                import './styles.scss';
                import './favicon.ico'; /* remove */
				

The files which need to be included in the bundled file in output have all been imported into the main index.js file.

core.js in index.js

the why


                import 'core-js/es6/map';
                import 'core-js/es6/set';
                

A polyfilled environment for React 16 using core-js to support older browsers. Visit the React documentation to learn more.

ReactDOM.render()

And the main App Component that holds everything else related to the app is rendered there as well.

Code/Bundle Splitting

is important for browser caching.

Browser Caching

is great because it allows sites to load faster once the client has grabbed the necessary files and assets from the server the first time a user visits the page.

Clearing The Cache

Caching Headaches

However, it can cause big headaches when new code has been added to files. If the name of a file remains the same when new code has been added to it, the browser thinks that nothing has changed. To the browser, same name means same content.

Output


                output: {
                    filename: PLATFORM === 'production' ? 'scripts/[name].[contenthash].chunk.js' : 'scripts/[name].chunk.js',
                    chunkFilename: PLATFORM === 'production' ? 'scripts/[name].[contenthash].chunk.js' : 'scripts/[name].chunk.js',
                    path: path.resolve(__dirname, '../dist')
                },
                

Once you've bundled all your assets together, you need to tell webpack where to bundle your app. The output object tells webpack what to do with bundled code. But this output object differs greatly from the first edition of this workflow.

filename


                output: {
                    filename: PLATFORM === 'production' ? 'scripts/[name].[contenthash].chunk.js' : 'scripts/[name].chunk.js',
                    chunkFilename: PLATFORM === 'production' ? 'scripts/[name].[contenthash].chunk.js' : 'scripts/[name].chunk.js',
                    path: path.resolve(__dirname, '../dist')
                },
                

The value of the first property, filename, refers to the name(s) of output bundle(s). Note the ternary operator.

filename


                output: {
                    filename: PLATFORM === 'production' ? 'scripts/[name].[contenthash].chunk.js' : 'scripts/[name].chunk.js',
                    chunkFilename: PLATFORM === 'production' ? 'scripts/[name].[contenthash].chunk.js' : 'scripts/[name].chunk.js',
                    path: path.resolve(__dirname, '../dist')
                },
                

Previously, when I used the CommonsChunkPlugin and not the SplitChunksPlugin, the name of the hash I used was [chunkhash:8]. Now that we all are using the SplitChunksPlugin with webpack, some of us use [contenthash]. For more about [contenthash] and webpack 4, click here.

chunkFilename


                output: {
                    filename: PLATFORM === 'production' ? 'scripts/[name].[contenthash].chunk.js' : 'scripts/[name].chunk.js',
                    chunkFilename: PLATFORM === 'production' ? 'scripts/[name].[contenthash].chunk.js' : 'scripts/[name].chunk.js',
                    path: path.resolve(__dirname, '../dist')
                },
                

chunkFilename determines the name of non-entry chunk files. In my configuration, that means either vendor chunks or (at least with this particular workflow) dynamic imports. Again, note the ternary operator.

path


                const path = require('path');
                output: {
                    filename: PLATFORM === 'production' ? 'scripts/[name].[contenthash].chunk.js' : 'scripts/[name].chunk.js',
                    chunkFilename: PLATFORM === 'production' ? 'scripts/[name].[contenthash].chunk.js' : 'scripts/[name].chunk.js',
                    path: path.resolve(__dirname, '../dist')
                },
                

The value of the third property, path, is the absolute path to the dist folder, where the bundled assets end up. It has to be required in your webpack config in order for you to able to use it.

path


                const path = require('path');
                output: {
                    filename: PLATFORM === 'production' ? 'scripts/[name].[contenthash].chunk.js' : 'scripts/[name].chunk.js',
                    chunkFilename: PLATFORM === 'production' ? 'scripts/[name].[contenthash].chunk.js' : 'scripts/[name].chunk.js',
                    path: path.resolve(__dirname, '../dist')
                },
            	

path.resolve() resolves a sequence of paths or path segments into an absolute path. As mentioned earlier, for the output path, webpack needs to know exactly where you want your bundles and output files to be emitted.

[name]


                output: {
                    filename: PLATFORM === 'production' ? 'scripts/[name].[contenthash].chunk.js' : 'scripts/[name].chunk.js',
                    chunkFilename: PLATFORM === 'production' ? 'scripts/[name].[contenthash].chunk.js' : 'scripts/[name].chunk.js',
                    path: path.resolve(__dirname, '../dist')
                },
                

I use the [name] property because of my bundle splitting resulting from naming different types of chunks. Previously the reasoning was because of > 1 entry point, but now it's because of > 1 type of chunk. As you saw, they do not reside in the entry point!

Bundle Splitting vs Code Splitting

With webpack 4, it has become clear to me that bundle splitting and code splitting are NOT interchangeable, as I previously thought. That's because of my introduction to dynamic imports, which IS code splitting, as opposed to multiple entry points, which is bundle splitting.

Bundle Splitting vs Code Splitting

I will be updating the terminology in the first edition of this workflow. I came across a fantastic article on medium that gets into the difference between the two, and helped me tremendously in optimizing my vendor bundle(s): The 100% correct way to split your chunks with Webpack, by David Gilbertson.

[name]


                output: {
                    filename: PLATFORM === 'production' ? 'scripts/[name].[contenthash].chunk.js' : 'scripts/[name].chunk.js',
                    chunkFilename: PLATFORM === 'production' ? 'scripts/[name].[contenthash].chunk.js' : 'scripts/[name].chunk.js',
                    path: path.resolve(__dirname, '../dist')
                },
                

[name] takes into consideration that there can be more than one type of main bundle file, and they may have different names.

[contenthash]


                output: {
                    filename: PLATFORM === 'production' ? 'scripts/[name].[contenthash].chunk.js' : 'scripts/[name].chunk.js',
                    chunkFilename: PLATFORM === 'production' ? 'scripts/[name].[contenthash].chunk.js' : 'scripts/[name].chunk.js',
                    path: path.resolve(__dirname, '../dist')
                },
                

A simple way to make sure the browser picks up changed files is by using output.filename substitutions. The [hash] (or previously for me [chunkhash]) can be used to include a build-specific hash in the filename.

[contenthash]


                output: {
                    filename: PLATFORM === 'production' ? 'scripts/[name].[contenthash].chunk.js' : 'scripts/[name].chunk.js',
                    chunkFilename: PLATFORM === 'production' ? 'scripts/[name].[contenthash].chunk.js' : 'scripts/[name].chunk.js',
                    path: path.resolve(__dirname, '../dist')
                },
                

It's even better to use the [contenthash] substitution, which is the hash of the content of a file, different for each type of asset.

Michael Ciniawsky, a webpack core contributor, put it well in webpack issue #7179:

[chunkhash] isn't very 'stable' as it can change when e.g a module of a chunk is moved for some reason ... For predictable long term (browser) caching the contents of the asset are the only reliable hashing source since browsers cache files based on Map { url => content } relations without any knowledge of webpack chunks or the like...

And for Code Splitting (Async Chunks):

Also don't forget to set the chunkFilename for Code Splitting (Async Chunks):


                const output = {
                    filename: dev || ci ? '[name].bundle.js' : '[name].[contenthash].bundle.js',
                    chunkFilename: dev || ci ? '[name].chunk.js' : '[name].[contenthash].chunk.js'
                }
                

< /> === 3

There are 3 main types of code in a typical app or site built with webpack:

  • The source code you have written
  • Third party, vendor code your source is dependent on
  • Webpack runtime and manifest that conducts the interaction of all modules

Runtime

The runtime, along with the manifest data, is basically all the code webpack needs to connect your modularized app while it's running in the browser.

Manifest

manifest refers to the collection of module related data runtime uses to resolve and load modules after they have been bundled and shipped to the browser.

Runtime/Manifest

Both will be discussed in detail when we get to webpack.prod.config.js.

Side Effects

So why mention all of this? Because runtime and manifest affect browser caching.

[chunkhash]

Previously when using [chunkhash], certain hashes change even when their content did not. This was caused by the injection of runtime and manifest, which changed every build.

CommonsChunkPlugin

Originally, chunks (and modules imported inside them) were connected by a parent-child relationship in the internal webpack graph. The CommonsChunkPlugin was used to avoid duplicated dependencies across them, but further optimizations were not possible. Since webpack v4, the CommonsChunkPlugin was removed in favor of optimization.splitChunks.

SplitChunksPlugin

We can use the webpack SplitChunksPlugin to split (vendor) dependencies into chunks.

SplitChunksPlugin

Out of the box, SplitChunksPlugin should work well for most users.

SplitChunksPlugin

By default it only affects on-demand chunks, because changing initial chunks would affect the script tags the html file should include to run the project.

SplitChunksPlugin

webpack will automatically split chunks based on the following conditions:

Conditions

  • When a new chunk can be shared OR modules are from the node_modules folder.
  • When a new chunk is bigger than 30kb (before min+gz)
  • The maximum number of parallel requests when loading chunks on demand would be lower or equal to 5.
  • The maximum number of parallel requests at initial page load would be lower or equal to 3.
  • When trying to fulfill the last two conditions, bigger chunks are preferred.

Extracting a Runtime

config/webpack.prod.config.js


                optimization: {
                    runtimeChunk: 'single',
                },
                

After adding runtimeChunk, a new (default name) runtime file will be created. The value "single" creates a single runtime file to be shared for ALL generated chunks.

Dev Output In Terminal


                10926 ± npm run start ✹

                > speech-to-text-app@0.0.1 start /Users/mariacam/Development/speech-to-text-app
                > webpack-dev-server --mode development --config config/webpack.base.config.js --open --hot --history-api-fallback
                --env.PLATFORM=local --env.VERSION=stag

                clean-webpack-plugin: /Users/mariacam/Development/speech-to-text-app/config/dist has been removed.
                ℹ 「wds」: Project is running at http://localhost:8080/
                ℹ 「wds」: webpack output is served from /
                ℹ 「wds」: 404s will fallback to /index.html
                ℹ 「wdm」: wait until bundle finished: /
                ℹ 「wdm」: Hash: cc60f0b1138d573ca57d
                Version: webpack 4.3.0
                Time: 7393ms
                Built at: 11/23/2018 3:29:44 PM
                Asset Size Chunks Chunk Names
                src/favicon.2cc0ddfd.ico 1.06 KiB [emitted]
                scripts/Speech.chunk.js 29.9 KiB Speech [emitted] Speech
                scripts/main.chunk.js 50.2 KiB main [emitted] main
                scripts/vendors~Speech.chunk.js 794 KiB vendors~Speech [emitted] vendors~Speech
                scripts/vendors~main.chunk.js 1.27 MiB vendors~main [emitted] vendors~main
                favicon.ico 1.06 KiB [emitted]
                index.html 485 bytes [emitted]
                precache-manifest.ead46283758951b05b27e4de5ca60d6c.js 655 bytes [emitted]
                sw.js 907 bytes [emitted]
                Entrypoint main = scripts/vendors~main.chunk.js scripts/main.chunk.js
                [./node_modules/core-js/es6/map.js] 208 bytes {vendors~main} [built]
                [./node_modules/core-js/es6/set.js] 208 bytes {vendors~main} [built]
                [./node_modules/webpack-dev-server/client/index.js?http://localhost:8080]
                (webpack)-dev-server/client?http://localhost:8080 7.78 KiB {vendors~main} [built]
                [./node_modules/webpack-dev-server/client/overlay.js] (webpack)-dev-server/client/overlay.js 3.58 KiB {vendors~main}
                [built]
                [./node_modules/webpack-dev-server/client/socket.js] (webpack)-dev-server/client/socket.js 1.05 KiB{vendors~main}
                [built]
                [./node_modules/webpack/hot sync ^\.\/log$] (webpack)/hot sync nonrecursive ^\.\/log$ 170 bytes {main} [built]
                [./node_modules/webpack/hot/dev-server.js] (webpack)/hot/dev-server.js 1.66 KiB {vendors~main} [built]
                [./node_modules/webpack/hot/emitter.js] (webpack)/hot/emitter.js 77 bytes {vendors~main} [built]
                [./node_modules/webpack/hot/log-apply-result.js] (webpack)/hot/log-apply-result.js 1.31 KiB {vendors~main} [built]
                [./node_modules/webpack/hot/log.js] (webpack)/hot/log.js 1.03 KiB {vendors~main} [built]
                [0] multi (webpack)-dev-server/client?http://localhost:8080 (webpack)/hot/dev-server.js ./src/index.js 52 bytes {main}
                [built]
                [./src/App.js] 1.04 KiB {main} [built]
                [./src/favicon.ico] 70 bytes {main} [built]
                [./src/index.js] 382 bytes {main} [built]
                [./src/styles.scss] 1.33 KiB {main} [built]
                + 117 hidden modules
                Child html-webpack-plugin for "index.html":
                1 asset
                Entrypoint undefined = index.html
                [./node_modules/html-webpack-plugin/lib/loader.js!./src/index.html] 504 bytes {0} [built]
                [./node_modules/lodash/lodash.js] 527 KiB {0} [built]
                [./node_modules/webpack/buildin/global.js] (webpack)/buildin/global.js 509 bytes {0} [built]
                [./node_modules/webpack/buildin/module.js] (webpack)/buildin/module.js 519 bytes {0} [built]
                ℹ 「wdm」: Compiled successfully.
                

Dev Output In Terminal

So much has changed even in Terminal output when running your React application in dev mode with webpack-dev-server. Everything configured in config/webpack.base.config.js prints out in Terminal. So cool, right?

custom script, webpack-dev-server


                10926 ± npm run start ✹

                > speech-to-text-app@0.0.1 start /Users/mariacam/Development/speech-to-text-app
                > webpack-dev-server --mode development --config config/webpack.base.config.js --open --hot --history-api-fallback
                --env.PLATFORM=local --env.VERSION=stag
                

npm run start is my custom script for running my app in dev mode using webpack-dev-server. webpack-dev-server --mode development runs the webpack-dev-server in development. webpack-dev-server provides a simple web server and ability to use live reloading in development only.

--mode development

custom script, webpack-dev-server


                new webpack.DefinePlugin({
                    'process.env.VERSION': JSON.stringify(env.VERSION),
                    'process.env.PLATFORM': JSON.stringify(env.PLATFORM)
                })
                

Sets process.env.PLATFORM on DefinePlugin to --mode development. it also enables NamedChunksPlugin and NamedModulesPlugin.

--mode development

custom script, webpack-dev-server


                new webpack.DefinePlugin({
                    'process.env.VERSION': JSON.stringify(env.VERSION),
                    'process.env.PLATFORM': JSON.stringify(env.PLATFORM)
                })
                

I define --mode development alongside ternary operators throughout webpack.base.config.js using the PLATFORM variable.

custom script, webpack-dev-server


                10926 ± npm run start ✹

                > speech-to-text-app@0.0.1 start /Users/mariacam/Development/speech-to-text-app
                > webpack-dev-server --mode development --config config/webpack.base.config.js --open --hot --history-api-fallback
                --env.PLATFORM=local --env.VERSION=stag
                

Next we have to tell webpack where to look for files. Since we are using a custom script to do this instead of the webpack config file, we first add the --config flag followed by config/webpack.base.config.js to let webpack know that webpack.base.config.js is the config file located in a folder called config.

custom script, webpack-dev-server


                10926 ± npm run start ✹

                > speech-to-text-app@0.0.1 start /Users/mariacam/Development/speech-to-text-app
                > webpack-dev-server --mode development --config config/webpack.base.config.js --open --hot --history-api-fallback
                --env.PLATFORM=local --env.VERSION=stag
                

We can only use this flag if there indeed IS a config folder that holds webpack.base.config.js.

custom script, webpack-dev-server


                10926 ± npm run start ✹

                > speech-to-text-app@0.0.1 start /Users/mariacam/Development/speech-to-text-app
                > webpack-dev-server --mode development --config config/webpack.base.config.js --open --hot --history-api-fallback
                --env.PLATFORM=local --env.VERSION=stag
                

The --config flag is followed by config/webpack.base.config.js to indicate the file to interpret in dev mode. The --open flag opens the application in your default browser.

custom script, webpack-dev-server


                10926 ± npm run start ✹

                > speech-to-text-app@0.0.1 start /Users/mariacam/Development/speech-to-text-app
                > webpack-dev-server --mode development --config config/webpack.base.config.js --open --hot --history-api-fallback
                --env.PLATFORM=local --env.VERSION=stag
                

The --hot flag enables HMR (Hot Module Replacement). HMR builds on top of WDS (webpack-dev-server). It makes it possible to swap modules live.

custom script, webpack-dev-server

style-loader

The reason I use style-loader in development, for example, is because theMiniCssExtractPlugin does NOT support HMR, but style-loader DOES. That makes it possible to update any changes in my styles without refreshing the page.

custom script, webpack-dev-server


                10926 ± npm run start ✹

                > speech-to-text-app@0.0.1 start /Users/mariacam/Development/speech-to-text-app
                > webpack-dev-server --mode development --config config/webpack.base.config.js --open --hot --history-api-fallback
                --env.PLATFORM=local --env.VERSION=stag
                

The --history-api-fallback flag makes 404s fallback to /index.html.

custom script, webpack-dev-server


                10926 ± npm run start ✹

                > speech-to-text-app@0.0.1 start /Users/mariacam/Development/speech-to-text-app
                > webpack-dev-server --mode development --config config/webpack.base.config.js --open --hot --history-api-fallback
                --env.PLATFORM=local --env.VERSION=stag
                

--env.PLATFORM=local lets webpack-dev-server know that it is running locally. Whatever is set to PLATFORM === 'development' in config/webpack.base.config.js will be triggered by npm run start.

custom script, webpack-dev-server


                10926 ± npm run start ✹

                > speech-to-text-app@0.0.1 start /Users/mariacam/Development/speech-to-text-app
                > webpack-dev-server --mode development --config config/webpack.base.config.js --open --hot --history-api-fallback
                --env.PLATFORM=local --env.VERSION=stag
                

--env.VERSION=stag refers to the staging environment. The staging environment here is in dev mode.

Dev Output In Terminal

dev instance info


                clean-webpack-plugin: /Users/mariacam/Development/speech-to-text-app/config/dist has been removed.
                

This line lets us know that the clean-webpack-plugin has removed any old file versions in the dist folder before a build process.

Dev Output In Terminal

dev instance info


                ℹ 「wds」: Project is running at http://localhost:8080/
                ℹ 「wds」: webpack output is served from /
                

The first line emitted by [wds] tells us on which localhost our app is running. the next lets us know the contentBase of the dev build. In the first edition, I added a devServer object to my webpack dev config, and contentBase was one of its properties. I set contentBase to './src/'. If it is not explicitly set, then it defaults to '/'.

Dev Output In Terminal

dev instance info


                ℹ 「wds」: 404s will fallback to /index.html
                

This line is emitted as a result of the --history-api-fallback flag.

Dev Output In Terminal

dev instance info


                ℹ 「wdm」: wait until bundle finished: /
                ℹ 「wdm」: Hash: cc60f0b1138d573ca57d
                

[wdm] stands for webpack-dev-middleware. It is not new to webpack 4, but it is new to me because of my new approach(es) to the workflow. It is a wrapper that emits files processed by webpack to a server. It is built into [wds], but is also available as a separate package to allow more custom options.

[wds]/[wdm]

The webpack-dev-server is a little Node.js Express server, which uses the webpack-dev-middleware to serve a webpack bundle. It also has a little runtime which is connected to the server via Sock.js. The server emits information about the compilation state to the client, which reacts to those events. You can choose between different modes, depending on your needs. - webpack wiki docs on Github

[wds][wdm]

It was very difficult to pin down current (official) documentation which would provide a good explanation of the [wdm] internal to [wds]. I don't know how 'current' the explanation I found in the webpack wiki is (it's a over a year old), but it still gives a good idea of the relationship between [wds] and [wdm].

Dev Output In Terminal

dev instance info


                ℹ 「wdm」: wait until bundle finished: /
                ℹ 「wdm」: Hash: cc60f0b1138d573ca57d
                

As the dev bundle is being built, [wdm] prints out the first message. After the build is complete, it prints out a compilation/build hash. Every time a change is made in the dev build, a new hash is emitted identifying that build.

Dev Output In Terminal

dev instance info


                Version: webpack 4.3.0124
                Time: 7393ms
                Built at: 11/23/2018 3:29:44 PM
                

Dev Output In Terminal

assets emitted

If you check to see how assets were set up in webpack.base.config.js, viewable in slide 60, this would all make sense.

Dev Output In Terminal

assets emitted

Also note src/favicon.2cc0ddfd.ico in slide 60. This is why I end up with a src folder in dist containing this file. I need to add it as the value of favicon in the HtmlWebpackPlugin configuration in order to inject favicon.ico into index.html.

NOTE : I removed favicon.ico from src/ and moved it into static/. Please visit the README.md for a more detailed explanation!

other webpack base configs


                optimization: {
                    splitChunks: {
                        chunks: 'all'
                    }
                },
                

The 'all' value means that chunks can be shared between async and non-async chunks, and this can be very powerful.

other webpack base configs


                new CopyWebpackPlugin([{
                    from: 'src/static'
                }]),
                

I didn't technically need the copy-webpack-plugin for the app I used towards this second edition, but normally I would (it's where I would place my images for example), so I added it to the workflow here. Basically it copies any assets in the static folder over to the dist folder.

other webpack base configs


                new WorkboxPlugin.GenerateSW({
                    swDest: 'sw.js'
                }),
                

This is something entirely new to the workflow, but not to CRA. It is the configuration for the workbox-webpack-plugin.

other webpack base configs


                new WorkboxPlugin.GenerateSW({
                    swDest: 'sw.js'
                }),
                

This configuration generates an sw.js (service-worker) file in production, and lets webpack know its destination, including the name of the file. The reason there is no dest folder is because if one was added (dist) to the 'path', then a new dist within dist would be created. webpack already knows its destination.

service worker

the why : precache files

If you want your web app to work offline or there are assets you know can be cached for a long time, pre-caching is the best approach. precaching a file ensures that a file is downloaded and cached before a service worker is installed, meaning that if your service worker is installed, your files will be cached.

workbox and precaching

the workbox-webpack-plugin provides an easy way to precache files, but it can also create a precache-manifest.json file that lists the files to precache. It provides a list of the files to be precached along with revisioning.

precache-manifest.json


                self.__precacheManifest = [
                  {
                    "url": "styles/main.690dfc9a9eacd0ee4d0f.css"
                  },
                  {
                    "url": "styles/Speech.690dfc9a9eacd0ee4d0f.css"
                  },
                  {
                    "revision": "2cc0ddfdc87f52e9d8c0c7d5b6efbebe",
                    "url": "src/favicon.2cc0ddfd.ico"
                  },
                  {
                    "revision": "c00558e3eb099318bfd7",
                    "url": "scripts/vendors~main.1ad1cd98b3cea083930b.chunk.js"
                  },
                  {
                    "revision": "91502a5c5a80b43265a1",
                    "url": "scripts/vendors~Speech.f0e3c9ecdeb61f482646.chunk.js"
                  },
                  {
                    "revision": "526080fd283e0b39d519",
                    "url": "scripts/runtime.f6f71c9f7a8cf14b349f.chunk.js"
                  },
                  {
                    "revision": "1365bbd7f1fd166121d6",
                    "url": "scripts/main.f8d88775b8bc47d47855.chunk.js"
                  },
                  {
                    "revision": "07fd12356b7d18233857",
                    "url": "scripts/Speech.d477efae3d7e20cf3ae3.chunk.js"
                  },
                  {
                    "revision": "23378fffa3b2d116a81dbb2052c30463",
                    "url": "index.html"
                  },
                  {
                    "revision": "2cc0ddfdc87f52e9d8c0c7d5b6efbebe",
                    "url": "favicon.ico"
                  }
                ];
                

precache-manifest.json

precache-manifest.json prints out the list of files being precached, basically all your app's assets (by default). It consists of two attribute-value pairs. The url attribute and its value, and the revisioning attribute and its value.

precache-manifest.json

url attribute-value pair

The url value is the path to the asset as it appears in the index.html tag. That usually does not change unless there is a complete overhaul of an app's workflow configuration!

precache-manifest.json

revisioning attribute-value pair

The revisioning value on the other hand, changes when there is a change to the asset's content. This way, old versions of a file will not continue to be cached. They are updated with the help of workbox and new caches are generated.

Build Output in Terminal


                npm run predeploy ⏎ ✹

                > speech-to-text-app@0.0.1 predeploy /Users/mariacam/Development/speech-to-text-app
                > webpack --mode production --config config/webpack.prod.config.js --env.PLATFORM=production --env.VERSION=stag
                --progress

                clean-webpack-plugin: /Users/mariacam/Development/speech-to-text-app/config/dist has been removed.
                0% compiling(node:29675) DeprecationWarning: Tapable.plugin is deprecated. Use new API on `.hooks` instead
                Hash: 690dfc9a9eacd0ee4d0f
                Version: webpack 4.3.0
                Time: 25239ms
                Built at: 11/23/2018 6:36:14 PM
                Asset Size Chunks Chunk Names
                styles/main.690dfc9a9eacd0ee4d0f.css 720 bytes 4 [emitted] main
                src/favicon.2cc0ddfd.ico 1.06 KiB [emitted]
                styles/Speech.690dfc9a9eacd0ee4d0f.css 1.39 KiB 1 [emitted] Speech
                scripts/Speech.d477efae3d7e20cf3ae3.chunk.js 7.32 KiB 1 [emitted] Speech
                scripts/runtime.f6f71c9f7a8cf14b349f.chunk.js 2.64 KiB 2 [emitted] runtime
                scripts/vendors~main.1ad1cd98b3cea083930b.chunk.js 129 KiB 3 [emitted] vendors~main
                scripts/vendors~Speech.f0e3c9ecdeb61f482646.chunk.js 26.6 KiB 0 [emitted] vendors~Speech
                scripts/main.f8d88775b8bc47d47855.chunk.js 1.27 KiB 4 [emitted] main
                favicon.ico 1.06 KiB [emitted]
                index.html 686 bytes [emitted]
                precache-manifest.25877dad8e187853834337f15891def2.js 967 bytes [emitted]
                sw.js 907 bytes [emitted]
                asset-manifest.json 702 bytes [emitted]
                Entrypoint main = scripts/runtime.f6f71c9f7a8cf14b349f.chunk.js scripts/vendors~main.1ad1cd98b3cea083930b.chunk.js
                styles/main.690dfc9a9eacd0ee4d0f.css scripts/main.f8d88775b8bc47d47855.chunk.js
                [+EN/] ./src/styles.scss 39 bytes {4} [built]
                [NX+1] ./src/App.scss 245 bytes {4} [built]
                [Z1A6] ./src/components/speech/Speech.js + 5 modules 12.9 KiB {1} [built]
                | 6 modules
                [m1cd] ./src/favicon.ico 70 bytes {4} [built]
                [tjUo] ./src/index.js + 1 modules 1.45 KiB {4} [built]
                | ./src/index.js 382 bytes [built]
                | ./src/App.js 1.04 KiB [built]
                [yLpj] (webpack)/buildin/global.js 509 bytes {0} [built]
                + 89 hidden modules
                Child mini-css-extract-plugin
                node_modules/css-loader/index.js??ref--6-1!node_modules/postcss-loader/src/index.js!node_modules/sass-loader/lib/loader.js!src/App.scss:
                Entrypoint mini-css-extract-plugin = *
                [bUMP]
                ./node_modules/css-loader??ref--6-1!./node_modules/postcss-loader/src!./node_modules/sass-loader/lib/loader.js!./src/App.scss
                2.47 KiB {0} [built]
                + 1 hidden module
                Child mini-css-extract-plugin
                node_modules/css-loader/index.js??ref--6-1!node_modules/postcss-loader/src/index.js!node_modules/sass-loader/lib/loader.js!src/styles.scss:
                Entrypoint mini-css-extract-plugin = *
                [OL5h]
                ./node_modules/css-loader??ref--6-1!./node_modules/postcss-loader/src!./node_modules/sass-loader/lib/loader.js!./src/styles.scss
                959 bytes {0} [built]
                + 1 hidden module
                Child html-webpack-plugin for "index.html":
                1 asset
                Entrypoint undefined = index.html
                [MFLM] ./node_modules/html-webpack-plugin/lib/loader.js!./src/index.html 504 bytes {0} [built]
                [YuTi] (webpack)/buildin/module.js 519 bytes {0} [built]
                [yLpj] (webpack)/buildin/global.js 509 bytes {0} [built]
                + 1 hidden module
                Child mini-css-extract-plugin
                node_modules/css-loader/index.js??ref--6-1!node_modules/postcss-loader/src/index.js!node_modules/sass-loader/lib/loader.js!src/components/speech/Speech.scss:
                Entrypoint mini-css-extract-plugin = *
                2 modules
                

custom script, npm run predeploy


                npm run predeploy ⏎ ✹

                > speech-to-text-app@0.0.1 predeploy /Users/mariacam/Development/speech-to-text-app
                > webpack --mode production --config config/webpack.prod.config.js --env.PLATFORM=production --env.VERSION=stag
                --progress
                

npm run predeploy is the custom script I run before I deploy to gh-pages. It builds my dist folder required for deployment (or re-deployment) to gh-pages. This script is shorter than npm run start, and some things should also look familiar.

--mode production

custom script, npm run predeploy


                npm run predeploy ⏎ ✹

                > speech-to-text-app@0.0.1 predeploy /Users/mariacam/Development/speech-to-text-app
                > webpack --mode production --config config/webpack.prod.config.js --env.PLATFORM=production --env.VERSION=stag
                --progress
                

Now, instead of webpack-dev-server command, we use the webpack commmand. In order to properly use the webpack command, we need to have the webpack-cli installed as a devDependency in our app.

--mode production

custom script, npm run predeploy

This is a change from the first edition, where I used TARGET_ENV=development in my custom npm run serve script, declared and defined a variable TARGET_ENV in webpack-dev.config.js, and new webpack.DefinePlugin({DEV: true}).

--mode production

custom script, npm run predeploy

I also did not install or use the webpack-cli in the first edition, which can be limiting.

--mode production

custom script, npm run predeploy


                webpack --mode production --config config/webpack.prod.config.js --env.PLATFORM=production --env.VERSION=stag
                

This line is similar to the one for webpack-dev-server, but for production. The --progress flag allows you to see the (%) progress of your production build.

Build Output In Terminal

assets emitted

Again, if you check to see how assets were set up in webpack.base.config.js, viewable in slide 60, assets emitted would mostly make sense. Specifically, non node_module related .js files/chunks and .css files/chunks. But there is one type of file that throws many people off and that I wanted to discuss only in production because of its configuration in config/webpack.prod.config.js. Can you guess which one that is?

Build Output In Terminal

assets emitted: bundle splitting


                scripts/vendors~main.1ad1cd98b3cea083930b.chunk.js 129 KiB 3 [emitted] vendors~main
                scripts/vendors~Speech.f0e3c9ecdeb61f482646.chunk.js 26.6 KiB 0 [emitted] vendors~Speech
                

config/webpack.prod.config.js


                const webpack = require('webpack');
                const merge = require('webpack-merge');

                const ManifestPlugin = require('webpack-manifest-plugin');
                const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
                const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
                const Visualizer = require('webpack-visualizer-plugin');

                const baseConfig = require('./webpack.base.config');

                const prodConfiguration = env => {
                    return merge([
                        {
                            optimization: {
                                splitChunks: {
                                    chunks: 'all',
                                    maxInitialRequests: Infinity,
                                    minSize: 0,
                                    cacheGroups: {
                                        vendor: {
                                            test: / [\\/]node_modules[\\/]/,
                                            name(module) {
                                                // get the name. E.g. node_modules/packageName/not/this/part.js
                                                // or node_modules/packageName
                                                const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];

                                                // npm package names are URL-safe, but some servers don't like @ symbols
                                                return `npm.${packageName.replace('@', '')}`;
                                            },
                                        },
                                        styles: {
                                            name: 'main',
                                            test: /\.css$/,
                                            chunks: 'all',
                                            enforce: true
                                        }
                                    },
                                },
                                runtimeChunk: 'single',
                                minimizer: [
                                    new UglifyJSPlugin(),
                                    new OptimizeCssAssetsPlugin()
                                ],
                            },
                            plugins: [
                                new ManifestPlugin({
                                    fileName: ('asset-manifest.json')
                                }),
                                new webpack.HashedModuleIdsPlugin(),
                                new Visualizer({ filename: './statistics.html'})
                            ]
                        }
                    ])
                }

                module.exports = env => {
                    return merge(baseConfig(env), prodConfiguration(env));
                }
                

Build Output In Terminal

(vendor) node_module bundle splitting

When I first tried to implement the approach I took in the first edition, the size of my vendor bundle was VERY BIG. It was > 2MB, way over the suggested size of <= 244KB. Then I tried to split up the vendor entry files into individual node_module dependencies, but that did no good.

Build Output In Terminal

(vendor) node_module bundle splitting

I started searching for a better way. At this point I knew that I should split the vendor bundle somehow into individual node_module chunks, but I just did not know how. I had to learn more about the new webpack in order to make that happen.

Build Output In Terminal

(vendor) bundle splitting

After further research into webpack and some google searches, I came across David Gilbertson's article The 100% correct way to split your chunks with Webpack. It was EXACTLY what I was looking for!

webpack.prod.config.js

what hasn't changed


                const webpack = require('webpack');
                const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
                const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
                

As in the first edition, we need to require webpack, optimize-css-assets-webpack-plugin, and the uglifyjs-webpack-plugin.

webpack.prod.config.js

what's new


                    const merge = require('webpack-merge');
                    const ManifestPlugin = require('webpack-manifest-plugin');
                    const Visualizer = require('webpack-visualizer-plugin');
                    const baseConfig = require('./webpack.base.config');
                

webpack.prod.config.js

what's new

  • require the merge-webpack plugin, since we are merging base with prod.
  • require the webpack-manifest-plugin so that we can create an asset-manifest.plugin on build.
  • require the webpack-visualizer-plugin to see which modules are taking up space and which might be duplicates.
  • require webpack.base.config.js so that we can merge it into webpack.prod.config.js.

webpack.prod.config.js


                    const prodConfiguration = env => {}
                

We pass env into prodConfiguration, but this time we do not have to differentiate between development and production as we did in baseConfig. By default, mode === production unless configured otherwise.

webpack.prod.config.js


                module.exports = env => {
                    return merge(baseConfig(env), prodConfiguration(env));
                }
                

This is the code responsible for merging baseConfig with prodConfiguration and passing env into those configs. Since we are exporting, we can use these configs in our package.json. In the first edition we were using global process.env, and did not have to export our env. This configuration does NOT use global process.env, so we have to export it to make it available globally.

webpack.prod.config.js

optimization


                optimization: {
                    splitChunks: {
                        chunks: 'all',
                        maxInitialRequests: Infinity,
                        minSize: 0,
                    }
                }
                

webpack 4 runs optimizations for you depending on the chosen mode. You can still manually configure and override them, as done here.

webpack.prod.config.js

splitChunks


                chunks: 'all',
                

Choosing chunks: 'all' means that chunks can be shared between async and non-async chunks. It also tells webpack to put everything in node_modules/ into a file called vendors~main.<[contenthash]>.chunk.js.

webpack.prod.config.js

splitChunks


                maxInitialRequests: Infinity,
                

By default, webpack sets maxInitialRequests to 3 in production, but since we are splitting up our vendors~main.<[contenthash]>.chunk.js into individual modules of an indefinite number, we override the default and set it to Infinity.

webpack.prod.config.js

splitChunks


                    minSize: 0,
                

The minSize refers to the minimum size in bytes for a chunk to be generated. Since we want to split ALL modules no matter what the size, we override the default and set the minSize to 0.

webpack.prod.config.js

cacheGroups


                cacheGroups: {
                    vendor: {
                        test: / [\\/]node_modules[\\/]/,
                        name(module) {
                            // get the name. E.g. node_modules/packageName/not/this/part.js
                            // or node_modules/packageName

                            // npm package names are URL-safe, but some servers don't like @ symbols
                            return `npm.${packageName.replace('@', '')}`;
                        },
                    },
                    styles: {
                        name: 'main',
                        test: /\.css$/,
                        chunks: 'all',
                        enforce: true
                    }
                }
                

webpack.prod.config.js

cacheGroups

cacheGroups tells webpack to create chunks based on some conditions. To learn more, please visit the webpack 4 docs.

webpack.prod.config.js

Configuring cacheGroups: vendor

The defaults assign all modules from node_modules to a cacheGroup called vendors and all modules duplicated in at least 2 chunks to a cacheGroup called default. So that's what default cacheGroups is all about! To learn more, please visit the SplitChunksPlugin docs for webpack 4.26.1, but a slightly different version (and target market) than the purely English docs.

webpack.prod.config.js

cacheGroups: vendor


                    test: / [\\/]node_modules[\\/]/,
                

Instead of excluding as we do with our loaders in webpack.base.config.js, we are testing for the /node_modules/ directory. In other words, our vendor cacheGroups refers to and is used for any module being loaded from node_modules/.

webpack.prod.config.js

cacheGroups: vendor


                name(module) {
                    // get the name. E.g. node_modules/packageName/not/this/part.js
                    // or node_modules/packageName
                    const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];
                    // npm package names are URL-safe, but some servers don't like @ symbols
                    return `npm.${packageName.replace('@', '')}`;
                },
                

the name function with the parameter module passed in to it, contains a return statement which will generate the name of a chunk if conditions set in the value of the packageName variable are met.

webpack.prod.config.js

cacheGroups: vendor


                name(module) {}
                

Providing a string or function to generate the name of a split chunk allows you to use a custom name. Here we are providing a function. To learn more, please click here.

webpack.prod.config.js

cacheGroups: vendor


                const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];
                

So why module.context? context is an absolute string to the directory that contains the entry files. Like:


                    module.exports = {
                        //...
                        context: path.resolve(__dirname, 'app')
                    };
                

webpack.prod.config.js

cacheGroups: vendor


                const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];
                

A context is created if your request contains expressions, so the exact module is not known on compile time.

webpack.prod.config.js

cacheGroups: vendor

webpack parses the require() call and extracts some information:


                Directory: ./template 
                Regular expression: /^.*\.ejs$/
                

webpack.prod.config.js

cacheGroups: vendor


                const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];
                

A context module is generated. It contains references to all modules in that directory that can be required with a request matching the regular expression.

webpack.prod.config.js

cacheGroups: vendor

So if module.context matches the regular expression passed to the .match() method, it will be returned via the following:


                    // npm package names are URL-safe, but some servers don't like @ symbols
                    return `npm.${packageName.replace('@', '')}`;
                

webpack.prod.config.js

cacheGroups: vendor


                    // npm package names are URL-safe, but some servers don't like @ symbols
                    return `npm.${packageName.replace('@', '')}`;
                

We remove any "@" from any package names, because some servers don't like the "@" symbol. Have you ever tried to search for info regarding a package where the name starts with "@"? The browser rejects it ! The same principle applies here.

webpack.prod.config.js

cacheGroups: styles


                styles: {
                    name: 'main',
                    test: /\.css$/,
                    chunks: 'all',
                    enforce: true
                }
                

webpack.prod.config.js

cacheGroups: styles


                name: 'main',
                

I wanted to name the beginning of my css main, so I added the name property for the styles cacheGroup. Otherwise, it would end up being a number id followed by [hash].[ext]. And I did not want that!

webpack.prod.config.js

cacheGroups: styles


                test: /\.css$/,
                

The test property checks for chunks that end in .css.

webpack.prod.config.js

cacheGroups: styles

chunks: 'all',

chunks: 'all' ensures that all css is extracted into one file. I dynamically imported my Speech.js into my top level App.js. Not only did I split the Speech.js code away from main[contenthash].chunk.js, but I also split away Speech.js's Speech.scss (compiled to Speech.css) from main.css.

webpack.prod.config.js

cacheGroups: styles

I ended up with 2 style chunks/files.

webpack.prod.config.js

cacheGroups: styles


                enforce: true
                

The enforce property set to true ensures that aside from the Speech.css file, all other styles would reside in one main.css file.

webpack.prod.config.js

optimization: runtimeChunk


                runtimeChunk: 'single',
                

creates a "single" runtime file to be shared with all generated chunks. I had an equivalent configuration in the first edition, and wanted to keep it that way!

webpack.prod.config.js

optimization: minimizer


                minimizer: [
                    new UglifyJSPlugin(),
                    new OptimizeCssAssetsPlugin()
                ],
                

optimization.minimize property by default tells webpack to minimize the js bundle using the UglifyjsWebpackPlugin. I am using two minimizers, one for js, and the other for css, so I use the optimization.minimizer property, an array.

webpack.prod.config.js

optimization: minimizer

Adding new UglifyJSPlugin() is probably a bit redundant (now that I know better), but I had to add it in the first edition, so it was a matter of habit!

webpack.prod.config.js

plugins


                    plugins: [
                        new ManifestPlugin({
                            fileName: ('asset-manifest.json')
                        }),
                        new webpack.HashedModuleIdsPlugin(),
                        new Visualizer({ filename: './statistics.html'}),
                        new BundleAnalyzerPlugin()
                    ]
                

webpack.prod.config.js

plugins


                new ManifestPlugin({
                    fileName: ('asset-manifest.json')
                }),
                

the webpack-manifest-plugin creates the asset-manifest.json that generates the manifest data mentioned in slide 91, slide 92, and slide 93.

webpack.prod.config.js

asset-manifest.json

An asset-manifest.json file is generated in the root ('/') directory with a mapping of all source file names to their corresponding output file. Like:


                                runtime.js": "scripts/runtime.f6f71c9f7a8cf14b349f.chunk.js",
                                "vendors~main.js": "scripts/vendors~main.1ad1cd98b3cea083930b.chunk.js",
                                

Please visit the asset-manifest.json source code.

webpack.prod.config.js

asset-manifest.json

As mentioned earlier, the runtime along with the manifest data residing in asset-manifest.json, is basically all the code webpack needs to connect your modularized app while it's running in the browser.

webpack.prod.config.js

asset-manifest.json

It contains the loading and resolving logic needed to connect your modules as they interact. This includes connecting modules that have already been loaded into the browser as well as logic to lazy-load the ones that haven't.

HashedModuleIdsPlugin

The build output in the first edition in Terminal viewable in here is in part possible thanks to the HashedModuleIdsPlugin. Note the 4 character hashes within brackets. Visit slide 51 to slide 55 of the first edition to learn more.

Publishing to gh-pages on Build


                    > speech-to-text-app@0.0.1 deploy /Users/mariacam/Development/speech-to-text-app
                    > gh-pages -d dist
                    Published
                

gh-pages -d dist is the value of my custom script npm run deploy. It is possible because of the gh-pages npm dependency.

Separating Concerns

The current build results in a separate file called asset-manifest.json containing the manifest data. In my previous workflow, a file called runtime.js, but no asset-manifest.json. Here there is a separation of concerns as both an asset-manifest.json and a runtime.js is created.

HtmlWebpackPlugin

Because my webpack configuration is using the HtmlWebpackPlugin, there is no need to worry about adding the script tag for the manifest. The plugin adds a reference to index.html.

HtmlWebpackPlugin


                    plugins: [
                        new HtmlWebpackPlugin({
                            template: 'src/index.html',
                            favicon: 'src/favicon.ico', /* remove */
                            styles: 'src/styles.css',
                            inject: true
                        }),
                    ]
                

My HtmlWebpackPlugin configuration in my config/webpack-base.config.js.

HtmlWebpackPlugin Configuration

template

is the webpack require path to the template.

HtmlWebpackPlugin Configuration

favicon

gives the given favicon path to the output html.

NOTE : I removed favicon.ico from src/ and moved it into static/. Please visit the README.md for a more detailed explanation!

HtmlWebpackPlugin Configuration

styles

gives the given styles path to the output html.

HtmlWebpackPlugin Configuration

inject

default is set to true. I always like to make sure that my JS assets are placed at the bottom of the body element, so I use true. You can check out other options in the HtmlWebpackPlugin docs.

Separating Concerns

Separating runtime code from vendor code is the first step towards making sure that the vendor hash does not change with each new build unless a new 3rd party plugin is added.

Separating Concerns

Using [contenthash] instead of [chunkhash] or [hash] is another step towards making sure that the vendor hash does not change with each new build unless a new 3rd party plugin is added.

Resolving Order


				[gQNZ] ./src/style/style.css 307 bytes {1} [built]
				[lVK7] ./src/index.js 846 bytes {1} [built]
				[olkN] ./src/store.js 354 bytes {1} [built]
				[pnOm] ./src/App.js 3.01 kB {1} [built]
				[tuRH] ./src/reducers/todo.js 874 bytes {1} [built]
				[z7yQ] ./src/logo.svg 3.52 kB {1} [built]
				+ 2 hidden modules
						

However, this bundle splitting is not enough. Each time a new build takes place, each module.id is incremented based on resolving order by default. When the resolving order is changed, the module.id changes.

Blocking Change

So how can we prevent vendor's [contenthash] from changing? There are two webpack plugins that can get us there. One is the NamedModulesPlugin, and the other is the HashedModuleIdsPlugin.

Named Modules Plugin


				new webpack.NamedModulesPlugin(),
				

Uses the path to the module instead of a numerical identifier. It's useful in development because the output is more readable, but it also takes a bit longer to run.

Update: This now can be removed in config/webpack.base.config.js as it is a default in dev mode.

HashedModuleIdsPlugin


				new webpack.HashedModuleIdsPlugin(),
				

Recommended for production builds. It causes hashes to be based on the relative path of the module, generating a 4 character string as the module id.

HashedModuleIdsPlugin

before

Let's say our webpack.prod.config.js doesn't include the HashedModuleIdsPlugin and we have made a change to our js code. we might have added another React component, a helper function, etc. We would expect the hashes of our main js bundle and our asset-manifest.json to change on production build, but not our vendor bundle. However, all three change. Why?

HashedModuleIdsPlugin

before

Because each webpack module.id changes whenever the resolving order is changed. The resolving order changes whenever the module.id has changed. The vendor bundle hash changes because its resolving order has changed and therefore its module.id as well.

HashedModuleIdsPlugin

before

  • main js bundle: changed because new content was added.
  • vendor bundle: changed because its module.id changed because its resolving order changed.
  • asset-manifest.json: changed because it contains a reference to a new module.

HashedModuleIdsPlugin

after

Vendor remains constant

Let's say we add the HashedModuleIdsPlugin to our webpack production config and then ran another build. Our hash should stay consistent between builds. If we were to make a change to our js code again, the vendor hash should still remain the same because no changes were made to the bundle. Check it for yourself!

Loaders

webpack loaders transform your files into modules as they are added to your dependency graph. Loaders have 2 main purposes:

  • Identify which file or files should be transformed by a loader (test property)
  • Transform those files so that they can be added to your dependency graph, and eventually your bundle.js. (use property)

MiniCssExtractPlugin

Replaced the ExtractTextPlugin in webpack 4.

MiniCssExtractPlugin

The webpack >= v4.0.0 'support' for extract-text-webpack-plugin was moved to mini-css-extract-plugin as a more lightweight approach for webpack >= v4.0.0 and ETWP entered maintenance mode... - Michael Ciniawsky, member, webpack-contrib

MiniCssExtractPlugin

Extracts CSS into separate files. It creates a CSS file per JS file which contains CSS. Great for React and CSS Modules!

MiniCssExtractPlugin

Supports on-demand-loading of CSS and Source Maps.

vs ExtractTextWebpackPlugin

  • Async loading
  • No duplicate compilation (performance)
  • Easier to use
  • Specific to CSS
  • Does NOT support HMR yet

MiniCssExtractPlugin

Because it does not support HMR, I have to use style-loader in dev mode. Otherwise, I would have to do a manual page refresh to view my style changes. If you are acquainted with the first edition of the workflow, you might remember that style-loader does support HMR.

MiniCssExtractPlugin


                    {
                        test: /\.(scss|sass|css)$/,
                        exclude: /node_modules/,
                        loaders: [
                            MiniCssExtractPlugin.loader,
                            {
                            loader: 'css-loader',
                            options: {
                                modules: true,
                                sourceMap: true,
                                importLoaders: 1,
                                localIdentName: '[local]___[hash:base64:5]'
                            },
                        },
                            'postcss-loader',
                            'sass-loader'
                        ]
                    },
                

CSS test property


                    test: /\.(scss|sass|css)$/,
                

This test property includes .scss, .sass, and .css extensions, because I use .scss in my React applications. That .scss then has to be compiled into .css. This is a much more efficient way of implementing the test property.

CSS exclude property


                            exclude: /node_modules/,
            				

MiniCssExtractPlugin.loader

How you call the MiniCssExtractLoader.

CSS Loader and CSS Modules

This workflow supports CSS Modules. In order to make that happen, we have to add the following options to our css-loader:


                    loader: 'css-loader',
                    options: {
                        modules: true,
                        sourceMap: true,
                        importLoaders: 1,
                        localIdentName: '[local]___[hash:base64:5]'
                    },
                

CSS Modules

the what

Some people want to keep their external stylesheets but want to localize their styles. That's where CSS Modules come in. To view my presentation entitled The Evolution in Design and Development: A Melting Pot, in which I devote a section on CSS Modules, please click here.

Note: as of 2.0, CRA supports CSS Modules.

CSS Modules

When you add support for CSS Modules, whatever styles you add to a particular React component's scss/css file will only affect the styling of that particular component. If you have a component called Speech, a js file called Speech.js, and stylesheet called Speech.scss, those styles will only affect Speech.js. That's because you are importing them specifically into Speech.js. You'll get a better understanding if you visit this project's repository and study the code.

SCSS, CSS Modules, and React

Let's use the same example for SCSS in React using CSS Modules, as we do in this project. We would import Speech.scss into Speech.js, and those styles would only affect Speech.js. But maybe we want to have styles global to the whole project that we don't import anywhere. As a matter of fact, we don't even change the syntax of the classNames. We would treat this global styling the same as we would in any application using SCSS. Please visit the Speech To Text App repository to see for yourself.

css hot loader

Supports hot module replacement (HMR) for an extracted css file. shepherdwind, the creator of css-hot-loader, came up with the idea because style-loader, which also can achieve css hot reload, needs to inject a style tag into index.html. And this can happen before JS scripts are loaded, resuslting in a page with no styles.

css-hot-loader on Github extract-text-webpack-plugin issue #30 extract-text-webpack-plugin issue #89

css hot loader

Does not play nice with the MiniCssExtractPlugin, so I have removed it from the workflow for now. Too bad. It is fantastic.

Hot Module Replacement

HMR exchanges, adds, or removes modules while an app is running, without a full reload. It can speed up development because:

  • Can retain app state which is lost during a full reload.
  • Save valuable development time by only updating what has changed.
  • Tweak styling faster.

[contenthash]

[contenthash] returns a hash specific to content. It is available for the extract-text-webpack-plugin only, and is the most specific hash option available in webpack. And it's great to use with css files for browser caching. The hash only changes when the css changes.

[contenthash]

Apparently, the webpack team has suggested using [contenthash]over [hash] for better long term caching with the MiniCssExtractPlugin. I'm willing to give it a try, as I already use webpack's new [contenthash] for JS bundles and it works beautifully.

Plugins

Loaders only execute a transform on a per-file basis. Plugins are most commonly used to perform actions and custom functionality on "compilations" or "chunks" of your bundled modules.

Require


				const htmlWebpackPlugin = require('html-webpack-plugin');
				plugins: [

				]
						

In order to use a plugin, you have to require it.

Add


                plugins: [
                    new HtmlWebpackPlugin({
                        template: 'src/index.html',
                        favicon: 'src/favicon.ico', /* remove */
                        styles: 'src/styles.css',
                        inject: true
                    }),
                ]
				

and then add it to the plugins array. And since you can use the same plugin many times in the same config for different purposes, you need to create an instance of it by calling it with new.

CRA approach

And CRA takes the scripts approach. A lot like creating scripts for Gulp or Grunt tasks.

Custom NPM Scripts


                "scripts": {
                    "test": "jest",
                    "lint": "eslint .",
                    "clean": "rimraf dist",
                    "cleanSrc": "rimraf dist/src",
                    "start": "webpack-dev-server --mode development --config config/webpack.base.config.js --open --hot
                    --history-api-fallback --env.PLATFORM=local --env.VERSION=stag",
                    "predeploy": "webpack --mode production --config config/webpack.prod.config.js --env.PLATFORM=production
                    --env.VERSION=stag --progress",
                    "deploy": "gh-pages -d dist"
                },
				

You can create custom, aka local, npm scripts in your package.json file. We went over most of these scripts. The rest were the same in the first edition of this workflow.

clean script


				script: {
				  "clean ": "rimraf dist"
				}
				

Is executed in Terminal by running npm run clean. rimraf is an npm plugin which refers to the rm -rf comand. It cleans everything within the specified directory and all its subdirectories. It's much faster than the clean-webpack-plugin, which does the same thing. Same as in the first edition.

cleanSrc script


                script: {
                    "cleanSrc": "rimraf dist/src",
                }
                

This script also has not changed since the first edition. I use it to get rid of the src folder which gets injected into dist on production build because of the presence of favicon.ico. I am successful in getting rid of it both in master and gh-pages in the first edition, but then I deploy differently there.

cleanSrc script


                script: {
                    "cleanSrc": "rimraf dist/src",
                }
                

In future apps, I probably will revert to the old way of deployment. It's much more complex, but provides a clean solution. It's not a big deal, but it bugs me that I have a folder which serves no purpose!

POSTCSS

In this workflow, I only use POSTCSS for autoprefixer. For that, I only need to add the postcss-loader in webpack.base.config.js and require autoprefixer in postcss.config.js.

Wrap Up

When I first started working on my React workflow, it was some time back. I even left it for a bit and then returned to continue. Much has changed since then.

CRA

CRA has piled on more features and documentation! Whether you end up using it or not, it is worth following them on Github, visiting their issues threads, and becoming acquainted with their documentation. They even suggest alternatives!

Custom React App - Amido

React

The React team has been sensitive to developers' needs. For the longest time, they were discussing how to better serve developers in development and production. According to threads I came across on Github, many developers were deploying development instead of production code without realizing it. So React responded by adding a little icon to React DevTools that would signal to developers whether their application was deployed with development or production code.

React DevTools

If a React application is deployed with development code, the icon appears red. If it is deployed with production code, it appears black. In addition, a new warning appears in the browser console telling you if your application is running with minified development code rather than production.

webpack

webpack has drastically improved their documentation, making it easier to master. If you haven't already checked it out, you should. They're doing a really great job!

Resources

Resources contd

Resources contd

Resources contd