Problem

Here is the problem I want to solve: I want to get a static website up and running that contains a react app, written in typescript. I want to have a local dev server I can run for fast development, and build a production version of the website for deployment on a remote server. What is the simplest webpack config I need to write in order to get this setup up and running.

Introduction

I often find when I want to start a new website, that the tutorials tend to be quite a cumbersome mess. Either they contain too many configuration parameters, or they are magically abstracted tools (*cough* create-react-app *cough*) which the developer pays for in the long run, because they have no idea what is going on underneath the covers. In my opinion, abstractions are good, but only if you know what is going on underneath to be able to debug the intricacies of the software in the future. I tend to work in an extremely simplistic, first-principled way when I build any of the things I build. Either I try to use tools that follow simplicity over complex abstractions or, if the tool is sufficiently useful, I fork the tool and try to use my own copy that I can step into using the debugger.

In this article I wanted to present the most trivial way one can get a quick react site going using webpack and typescript.

All the code for this article can be found in the following repository:

https://github.com/anuraags-website-org/simplest-webpack-boilerplate

Folder Structure

We first start with a high level view of the folder structure for our boilerplate, simple app:

I have tried to keep this app as trivial as possible while providing a working application. We have the standard github boilerplate in the root folder which includes the .gitignore file and the LICENSE and README.md files. There is also a package.json containing the npm dependencies and scripts, and a tsconfig.json containing the typescript compiler configuration. Finally, as I am a fan of yarn over npm, there is also a yarn-error.log containing any yarn errors and a yarn.lock for the dependency manager.

My aim with this app is to make all the source code and assets relevant to the site (which includes html, css, images, json files, etc.) contained in the src folder and imported where needed using the ES6 import syntax. I have yet to find a file that does not have a relevant webpack loader, so this keeps all my source and data in a clean structured format.

The site itself

The site is an extremely trivial “Hello World!” site. All the information required to render the site is located in the src folder. Here is the react app (contained within src/index.tsx ):

import * as React from 'react' ; import * as ReactDOM from 'react-dom' ; import { match , Route , RouteComponentProps , Switch } from 'react-router' ; import { BrowserRouter , Link } from 'react-router-dom' ; import * as styles from './index.css' ; class App extends React . Component { render ( ) { return ( < div className = { styles . app } > < h1 > Hello World ! < / h1 > < / div > ) ; } } ReactDOM . render ( < BrowserRouter > < Route path = "/" component = { App } / > < / BrowserRouter > , document . getElementById ( 'app' ) , ) ;

The only real dependencies it has are react , react-dom , react-router , and react-router-dom . I probably could have done away with the react-router-* dependencies as well but I left them in for reference.

I’ve become a big fan of css-modules recently, giving me the ability to write plain css rather than inlining it in jsx and losing all the benefits of the IDE. Thus the index.css will need to be imported into the react component using a css modules loader. As in index.tsx the index.css file is also extremely simple.

html, body { margin : 0 ; height : 100% ; } .app { height : 100% ; width : 100% ; background-color : #faf7fc ; display : flex ; flex-direction : column ; font-family : Roboto ; }

Finally, we have the index.html file which is used as a template for the HTMLWebpackPlugin. It doesn’t differ much from the standard template used by the webpack plugin, but I prefer to use my own to have greater control over it in the future:

<!DOCTYPE html> < html > < head > < title > Simple Webpack Boilerplate </ title > < meta name = " viewport " content = " width = device-width, initial-scale = 1.0 " > < link rel = " stylesheet " href = " https://use.fontawesome.com/releases/v5.0.12/css/all.css " integrity = " sha384-G0fIWCsCzJIMAVNQPfjH08cyYaUtMwjJwqiRKxxE/rx96Uroj1BtIQ6MLJuheaO9 " crossorigin = " anonymous " > </ head > < body > < div id = " app " style =" height : 100% ; display : flex " /> </ body > </ html >

As an example I have included a fontawesome import as I tend to import font awesome in most of my project for fast access to svg icons.

Webpack

The next step is setting up our webpack config. As can be seen in the folder tree above I have three webpack config files: a base webpack config which contains settings common to both dev and prod, a dev environment config, and a prod environment config. A description of each config file will be outlined below.

The following is the base config file:

var path = require ( 'path' ) ; var webpack = require ( 'webpack' ) ; var fs = require ( 'fs' ) ; var HtmlWebpackPlugin = require ( 'html-webpack-plugin' ) ; const { TsConfigPathsPlugin } = require ( 'awesome-typescript-loader' ) ; module . exports = { resolve : { extensions : [ '.ts' , '.tsx' , '.js' , '.jsx' ] , modules : [ path . resolve ( __dirname , '..' , 'src' ) , "node_modules" , ] , plugins : [ new TsConfigPathsPlugin ( { configFileName : path . resolve ( __dirname , '..' , 'tsconfig.json' ) , } ) ] } , entry : path . resolve ( __dirname , '..' , 'src' , 'index.tsx' ) , output : { publicPath : '/' , filename : '[name].js' , sourceMapFilename : '[name].[chunkhash].map' , } , plugins : [ new webpack . EnvironmentPlugin ( [ 'NODE_ENV' , ] ) , new HtmlWebpackPlugin ( { template : path . join ( __dirname , '..' , 'src' , 'index.html' ) , inject : 'body' } ) , ] , module : { rules : [ { test : [ / \ . ( ts | tsx ) $ / ] , loader : "awesome-typescript-loader" , exclude : /\.stories\.tsx/ , } , { test : /\.css$/ , include : path . join ( __dirname , '..' , 'src' ) , use : [ 'style-loader' , { loader : 'typings-for-css-modules-loader' , options : { modules : true , namedExport : true , } } ] } , { test : /\.tpl.html/ , loader : 'html-loader' , } , { test : /\.(svg)(\?.+)?$/ , loader : 'react-svg-loader' } , { test : /\.(ico|png|jpg|gif|eot|ttf|woff|woff2)(\?.+)?$/ , loader : 'url-loader?limit=50000' } ] } , }

The base config basically sets up the loaders required to load the various imported files within the source folder. Typescript files are loaded by the “awesome-typescript-loader”, css files are loaded by the “style-loader” and the “typings-for-css-modules-loader” so that proper declarations files are output for the typescript system. HTML files are loaded by the “html-loader”. I also use the “react-svg-loader” for svg files so that I can do things like import * as Graphic from 'graphic.svg' and then use the graphic directly as <Graphic /> . Finally, all other files are loaded using the “url-loader”.

The base config also sets up extensions that should be resolved automatically, and any common plugins. The modules object sets up paths for node resolution and the output object sets up what the javascript chunks are output to.

The dev config is then:

const baseConfig = require ( './webpack.config.base.js' ) ; module . exports = merge ( baseConfig , { plugins : [ new webpack . EnvironmentPlugin ( [ 'NODE_ENV' , ] ) , new HtmlWebpackPlugin ( { template : path . join ( __dirname , '..' , 'src' , 'index.html' ) , inject : 'body' } ) , ] , devServer : { contentBase : path . resolve ( __dirname , '..' , 'src' ) , publicPath : '/' , historyApiFallback : true , proxy : { '/_a' : { target : 'http://localhost:3000' , pathRewrite : { '^/_a' : '' } , } , } , } , } ) ;

The only functionality of the dev config is to set up a webpack-dev-server. The prod config then becomes:

const baseConfig = require ( './webpack.config.base.js' ) ; module . exports = merge ( baseConfig , { output : { publicPath : '' , filename : '[name].bundle.[chunkhash].js' , sourceMapFilename : '[name].[chunkhash].map' , } , plugins : [ new webpack . EnvironmentPlugin ( [ 'NODE_ENV' , ] ) , new UglifyJsPlugin ( { sourceMap : false , parallel : true , } ) , new webpack . LoaderOptionsPlugin ( { minimize : true , } ) , ] } ) ;

The only functionality of the prod server is to uglify and minify the source code. By default webpack outputs production code to the dist folder.

package.json setup

Finally, we have the package.json setup:

{ "name" : "simplest-webpack-boilerplate" , "version" : "1.0.0" , "description" : "Repository for \"The Simplest Webpack Build\" article" , "main" : "src/index.tsx" , "repository" : "https://github.com/anuraags-website-org/simplest-webpack-boilerplate.git" , "author" : "Anuraag Sridhar" , "license" : "MIT" , "engines" : { "node" : ">=8.9.0" } , "engineStrict" : true , "scripts" : { "start:dev" : "NODE_ENV=development webpack-dev-server --history-api-fallback --inline --hot --config ./config/webpack.config.dev.js --mode development" , "build:dist" : "NODE_ENV=production yarn webpack --progress --config ./config/webpack.config.prod.js --mode production" } , "devDependencies" : { "@types/classnames" : "^2.2.6" , "@types/react-dom" : "^16.0.7" , "@types/react-router" : "^4.0.30" , "@types/react-router-dom" : "^4.3.0" , "awesome-typescript-loader" : "^5.2.0" , "babel-core" : "^6.26.3" , "babel-runtime" : "^6.26.0" , "clean-webpack-plugin" : "^0.1.19" , "css-loader" : "^1.0.0" , "file-loader" : "^2.0.0" , "html-loader" : "^0.5.5" , "html-webpack-plugin" : "^3.2.0" , "react" : "^16.4.2" , "react-dom" : "^16.4.2" , "react-router" : "^4.3.1" , "react-router-dom" : "^4.3.1" , "react-svg-loader" : "^2.1.0" , "style-loader" : "^0.23.0" , "typescript" : "^3.0.3" , "typings-for-css-modules-loader" : "^1.7.0" , "url-loader" : "^1.1.1" , "webpack" : "^4.17.1" , "webpack-cli" : "^3.1.0" , "webpack-dev-server" : "^3.1.7" , "webpack-merge" : "^4.1.4" } }

This is the bare minimum required to get everything in this project up and running. The packages are restricted to only the necessary packages for the source code to compile, along with the loaders and plugins required for webpack. There are only two main scripts: start:dev which runs a webpack dev server that serves up the website, and build:dist which outputs a static version of the website, to the dist directory, that can be served up by any web server (e.g. cd dist; python -m SimpleHTTPServer ).

Hope this gets someone up and running on fronted website.