A template for building an SPA with minimal production footprint.
- Yarn
- Webpack 5+
- Babel 7.10+
- Typescript 4.1+
- ESLint
- Prettier
- React via CDN (Guide to easily switch to Preact)
An alternative to Create React App (CRA) it is designed for personal use, but available for others to use.
- Code splitting (provided by webpack) when dynamic imports
import()
is used - Using a
.env
file to set your environment variables. import
ing images into code- Browserlist support
- Eslint linting and prettier with pre-commit hooks to ensure code quality
- Container (dockerfile) support
- Bundle analysis
Note: This project intentionally offers minimal features its build, but with guides to add features, such as styling, because many projects require different use cases.
-
Get code
# Clone git clone [email protected]:lski/webpack-babel-typescript-react-template.git # Remove the remote, which points at the template code git remote remove origin # Update packages yarn upgrade-interactive --latest # or `yarn outdated`
-
Change fields in
package.json
:- name
- description
- contributors
- keywords
-
Remove everything above this comment ;) and replace it with your project information :)
To run the application the scripts are similar to those of Create React App.
-
yarn run start
Starts a development build usingwebpack-dev-server
-
yarn run test
Runs tests -
yarn run build
Creates a minified production ready build -
yarn run build:dev
Similar tobuild
but creates an unoptimised development ready build. -
yarn run lint
Runs eslint on the code, highlighting any errors/warnings. -
yarn run lint:fix
Applies auto fixes, where possible, to errors/warnings found byyarn run lint
. -
yarn run analysis
Uses
webpack-bundle-analyzer
to create a report on both the development & production builds. To view the reports navigate to/report
and open the html files.
All settings are optional and are set using the command line (via webpacks --env flag e.g. --env buildPath=./build
) or environment variable.
Note: Command line has priority over Environment Variables.
Setting | Default | |
---|---|---|
buildPath | ./build |
The output directory for all built assests. Gets cleaned (emptied) prior to new build. Can be relative or absolute. |
publicUrl | / |
When the app is built it sets a prefix for where the static assets are located so they can be found in the browser. Normally that defaults to the current url base of that 'page' e.g. app.js is referenced <script src="app.js"></script> but sometimes the files will be stored in a CDN or that the app itself is running from a sub path of the current domain e.g. https://my-domain/my-app/ .This setting allows a prefix to be set for the assets output the the html file. E.g. publicUrl=https://a-cdn/ would result in https://a-cdn/app.js .The public Url can also be used throughout the application, it can be referenced directly in js/ts files via process.env.PUBLIC_URL or in the html template via <%= PUBLIC_URL %> . Note: The value gets passed on to webpack publicPath and it must end in a forward slash / unless an empty value. |
analysis | false |
Creates a bundle report for the current build. See yarn run analysis |
host | 0.0.0.0 |
The host to run the webpack-dev-server on, only used when using yarn run start . The default option exposes it on localhost and externally via machine IP. |
port | 3030 |
The port to run the webpack-dev-server on, only used when using yarn run start . |
Setting | Environment Variable |
---|---|
buildPath | BUILD_PATH |
publicUrl | PUBLIC_URL |
analysis | BUILD_ANALYSIS=true |
host | HOST |
port | PORT |
A single .env
file is supported fro development, however unlike CRA there is no support for multiple versions of .env
as per the recommendation by the dotenv package.
It is recommendation to use a .env file for defaults, but override the settings using the command line options for different environments. In CI environments it is recommended to simply use environment variable directly. The .env
file is excluded from git, so will not effect other machines.
Like with Create React App it is possible to use custom environment variables them directly in your code! However as pointed out by Create React App, exposing all the environment variables for a system would be a security risk, so only the only environment varibles available to your application are NODE_ENV
, PUBLIC_URL
and any environment variable that starts WPT_APP_
will be available in the app.
E.g. An environment variable: [email protected]
could be referenced directly in code with process.env.WPT_APP_ADMIN_EMAIL
.
Types for Typescript are loaded from 2 folders: /node_modules/@types
and /src/types
to add more declaration files add them to the types folder and Typescript should reference them directly.
This application supports both developing your application in a Docker container and also running a production version.
# Build the docker image
docker build -f ./Dockerfile.dev --tag wpt:dev .
# Run the docker image as a container
docker run -p 8080:80 -v $PWD:/app/ -d --name wptdev wpt:dev
Explanation
We build a new docker image using the Dockerfile.dev
configuration file and tag it a name (wpt) and a version (dev) wpt:dev
which can be anything you want and should be change to be applicable to your application. The 'dev' allows you to avoid hitting production versions on your machine.
We then create a new container and run it giving the new container a name (wptdev), which should be unique to this container.
By using the volume command -v $PWD:/app/
we tell docker to bind the app folder in the container to our file application. Now any changes you make in the application code will fire a webpack dev server rebuild meaning the website updates!
Tip: You might get a conflict from an existing 'wptdev' container, if that happens it means you still have a wptdev container running and need to close it down and remove it.
# Stop (forceably) and remove the container
docker rm -f $(docker ps -aq --filter name=wptdev)
To build a production version and run:
# Build the image.
docker build --tag wpt:1.0.0 .
# Run the image as a container
docker run -p 8080:80 -d --name wpt-1 wpt:1.0.0
Explanation
We start by creating an image with a name (wpt) and version (1.0.0) that can be changed as needed Note: You should increment your version numbers as you make changes to avoid conflicts.
We create and run a container, calling it wpt-1
from the production build 1.0.0
we created earlier. Remember that container names should be unique, so if you are going to run multiple containers then remember to change the name for each e.g. wpt-2, wpt-3, etc.
We have exposed the nginx website inside the container on port 8080 on out machine, like we did for the dev build. Make sure you dont try runnign multiple containers on the same port!
Switch to Preact
Preact is a much smaller, and simplier, implementation of React and for small/medium projects just as good.
There are some limitations however, as of 10.4.1, Suspense
/lazy
is not fully stable yet, so requires a fallback to an asyncComponent
implementation or the @loadable/component
package.
You can use preact in several ways. You can use a CDN see this github comment and have it as an external
package. It is also possible to use it as 'preact' or to use it was a drop in replacement to React, so that it can be used with React plugins e.g. React Router.
If you want to use preact as preact and not as react, then update the tsconfig.json file as shown here. This project uses a babel toolchain to convert jsx, rather than typescript so keep it as jsx: "preserve"
as per the instructions.
Below is a guide to add preact as a drop in for react.
-
Install
preact
yarn add preact yarn remove react
Note: We dont remove the
react-dom
package, because we have used aliases it wont be picked up by webpack, it tricks typescript into thinking it exists. -
Create a new build configuration for preact to tell it to pretend to be react:
// .webpack/webpack.preact.cjs const preact = () => ({ resolve: { alias: { react: 'preact/compat', 'react-dom/test-utils': 'preact/test-utils', 'react-dom': 'preact/compat', // Must be below test-utils }, }, }); module.exports = { preact };
-
In the top level
build
function switch react config for preact// webpack.config.cjs const preact = require('./.webpack/webpack.preact.cjs'); // ...other code let config = combine( base(pageTitle), // react(), preact() // ... other configurations );
-
Remove (or comment out) external CDN script tags for React
<!-- public/index.html --> <!-- <script crossorigin src="https://unpkg.com/react@17/umd/react.production.min.js"></script> <script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script> -->
-
(Optional) Add loadable to make up for Suspense/lazy
yarn add @loadable/component && yarn add @types/loadable__component -D
-
(Optional) Add the ability to use Preact DevTools
// Add to the top of `src/index.tsx` if (process.env.NODE_ENV === 'development') { require('preact/debug'); }
Note: Preact has its own dev tools extension.
Add React Router
Just install the packages to use React Router
-
Install
react-router
/react-router-dom
along with types for Typescript```bash yarn add react-router-dom yarn add -D @types/react-router @types/react-router-dom ```
Add Jest and React Testing Library
This follows the guide on the jest website for adding it to a webpack project, as it need to handle assets that are not just typescript or javascript files. See the docs on [React testing library] to see how to use it and for a a list of additional jest matchers see the testing-library/jest-dom project.
The jest library by default runs any files that are either in a __tests__
folder or the filename finishes with .test.ts
.
*NB: See the jest
branch for a working version with jest.*
-
Install packages
yarn add -D jest babel-jest @types/jest @testing-library/jest-dom @testing-library/react identity-obj-proxy
-
Create setup file for jest
/jest.setup.ts
// Adds matchers to jest e.g. toBeInDocument() import '@testing-library/jest-dom';
-
Create a mock file for raw file importing e.g. images
/mocks/fileMock.ts
export default '';
-
Add configuration for jest to
/package.json
:{ // ...other settings "jest": { "moduleNameMapper": { "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/mocks/fileMock.ts", "\\.(css|less|scss)$": "identity-obj-proxy" }, "setupFilesAfterEnv": ["<rootDir>/jest.setup.ts"] } }
We are doing 3 things here:
- First we tell jest when it hits a raw file, like an image file, to instead use
fileMock.ts
, which just returns an empty string as normally webpack would be returning a string that can be used in a 'src' parameter and this allows jest to not worry about parsing it. - Second, we use the package
identity-obj-proxy
for any css (sass or less) files. As css/sass/less modules normally return objects with class names, identity-obj-proxy allows us to fake those objects.identity-obj-proxy
is not strictly needed for this, but its super lightweight and only included in tests. - Third are are creating a 'setup' file that jest will run after the environment is setup, so that we can add the matchers from testing-library/jest-dom to jests expect function.
- First we tell jest when it hits a raw file, like an image file, to instead use
-
Update the
test
script in/package.json
. Swap"test": "echo \"Error: no test specified\" && exit 1"
with"test": "jest"
-
Update
Dockerfile
to run tests on build prior to production:# ...Other commands # Run test and abort on error RUN yarn run test # Build the app RUN yarn run build
-
(Optional) Create a test file to test App is loading correctly:
import React from 'react'; import App from './App'; import { render, screen, cleanup, fireEvent } from '@testing-library/react'; afterEach(cleanup); test('App loads with correct text on button', () => { render(<App />); const element = screen.getByText(/click me/i); expect(element).toBeInTheDocument(); }); test('Clicking button changes text', () => { render(<App />); const element = screen.getByText(/click me/i); fireEvent.click(element); expect(element.textContent).toMatch(/clicked/i); });
Add StyledComponents
Styled Components are great as they enable putting real css in the same file as the Components they are used with.
In reality they dont require a build step, but adding the plugin is recommended as it makes Components easier to see in DevTools.
Note: node-sass
is not required for styled-components or emotion.
-
Install Styled Components
yarn add styled-components yarn add @types/styled-components -D
-
To fix a long standing bug in
@types/styled-components
add a.yarnclean
file:@types/react-native
-
(Optional but recommended) Add plugin to help with correctly named components in DevTools:
The plugin offers quite a few benefits, such as minification and help with debugging [see the website](https://styled-components.com/docs/tooling#babel-plugin) for more details and options. ```bash yarn add babel-plugin-styled-components -D ``` ```js // babel.config.js { // other settings "plugins": [ // other plugins "babel-plugin-styled-components" ] } ``` _**Note:** Avoid the plugin `typescript-plugin-styled-components` it seems more obvious than `babel-plugin-styled-components` but we are using babel to transpile the typescript, not ts-loader, so it is not applicable._
Add Emotion
Emotion is very similar to Styled Components, with different trade offs, like it has support for React's concurrency, it also has opt-in for different usages (e.g. css prop or styled) so a smaller footprint and has better TypeScript support. But on the negative still has the component tree of death that styled-components has removed.
-
Install emotion:
yarn add @emotion/react yarn add -D @emotion/babel-plugin
-
(Optional) Install styled
yarn add @emotion/styled
-
Add emotion to
babel.config.js
{ "presets": [ //other presets [ "@babel/preset-react", { "runtime": "automatic", "importSource": "@emotion/react" } ] ], "plugins": [ "emotion" // other plugins ] }
NB: Here we not only add the emotion plugin to babel, but we update the
@babel/preset-react
to tell it to handle emotionsjsx()
function rather than react (or preact) version ofjsx()
to support thecss
prop in components. -
Tell Typescript about the change of jsx function:
{ "compilerOptions": { // ... "jsx": "react-jsx", "jsxImportSource": "@emotion/react" // ... } }
References for emotion:
Add CSS and CSS Modules
To be able to import CSS files directly into your code and to take advantage of CSS Modules:
- Install dependencies
- Add config section for css
- Add config section to the pipeline
You can then use .css
and .module.css
files to your projects and they will be imported.
-
Install dependencies:
yarn add -D css-loader typings-for-css-modules-loader style-loader @teamsupercell/typings-for-css-modules-loader
-
Create a new build file for css
./webpack/webpack.css.js
:// ./webpack/webpack.css.js const css = () => ({ plugins: [ // WatchIgnorePlugin currently only used only to prevent '--watch' being slow when using Sass/CSS Modules, remove if not needed new WatchIgnorePlugin({ paths: [/css\.d\.ts$/] }), ], module: { rules: [ // Handles css style modules, requires an extension of ***.module.scss { exclude: [/node_modules/], test: /\.module.css$/, use: [ 'style-loader', '@teamsupercell/typings-for-css-modules-loader', { loader: 'css-loader', options: { modules: { localIdentName: '[name]__[local]--[hash:base64:5]', exportLocalsConvention: 'camelCase', }, }, }, ], }, // Handles none module css files { exclude: [/node_modules/], test: /(?<!\.module)\.css$/, use: [ 'style-loader', '@teamsupercell/typings-for-css-modules-loader', { loader: 'css-loader', options: { modules: { exportLocalsConvention: 'camelCase', }, }, }, ], }, ], }, }); module.exports = { css };
-
Add that config to the top level build function pipeline:
// webpack.config.cjs const css = require('./.webpack/webpack.css.cjs'); // ...other code let config = combine( base(pageTitle), // other configurations css() );
Note: Generally we would exclude auto generated files from git in the .gitignore
file. However on 'first build' types for the css modules files are not created by the plugin until after the build, meaning it will possibly fail in CI builds, so its not recommended.
Add Sass & Sass Modules
Similar to the steps to add CSS files directly to be able to import CSS files directly into your code and to take advantage of SASS Modules:
- Install dependencies
- Add config section for css
- Add config section to the pipeline
You can then use .scss
and .module.scss
files to your projects and they will be imported.
-
Install dependencies:
yarn add -D css-loader typings-for-css-modules-loader style-loader @teamsupercell/typings-for-css-modules-loader node-sass sass-loader
-
Create a new build file for sass
./webpack/webpack.sass.js
:// ./webpack/webpack.sass.js const sass = () => ({ plugins: [ // WatchIgnorePlugin currently only used only to prevent '--watch' being slow when using Sass/CSS Modules, remove if not needed new WatchIgnorePlugin({ paths: [/scss\.d\.ts$/] }), ], module: { rules: [ // Handles sass modules, requires an extension of ***.module.scss { exclude: [/node_modules/], test: /\.module.scss$/, use: [ 'style-loader', '@teamsupercell/typings-for-css-modules-loader', { loader: 'css-loader', options: { modules: { localIdentName: '[name]__[local]--[hash:base64:5]', exportLocalsConvention: 'camelCase', }, }, }, 'sass-loader', ], }, // Handles none module scss files { exclude: [/node_modules/], test: /(?<!\.module)\.scss$/, use: [ 'style-loader', '@teamsupercell/typings-for-css-modules-loader', { loader: 'css-loader', options: { modules: { exportLocalsConvention: 'camelCase', }, }, }, 'sass-loader', ], }, ], }, }); module.exports = { sass };
-
Add that config to the top level build function pipeline:
// webpack.config.cjs const css = require('./.webpack/webpack.sass.cjs'); // ...other code let config = combine( base(pageTitle), // other configurations sass() );
Note: Generally we would exclude auto generated files from git in the .gitignore
file. However on 'first build' types for the css modules files are not created by the plugin until after the build, meaning it will possibly fail in CI builds, so its not recommended.
It would be ideal if:
- Handle proxying api/server calls
- Handle auto for public_url setting
- I will add a module/nomodule split for output as soon as it lands in webpack 5+
- Attempt to combine Dockerfile.dev into Dockerfile
- This project either prepared for testing or added generic testing in ready for the developer, but need to decide on Cypress or Jest.
- Add manifest files to public for PWA support
- Add favicon to public
- Add it to index.html
- Consider using TS throughout for building the code. E.g. ts-node
- Do more tests on exporting fonts to the outputDir
- Investigate source maps relating to the original, rather than webpack output
- Add setting for dataurl size
- Add a baseUrl setting (in a similar way to the way PUBLIC_URL works for CRA)
- Consider the ExtractTextPlugin for CSS/SASS imports (Note: The benefits arent as good as first seems.)
- Look at setting for having the
fork-ts-checker-webpack-plugin
fail if using with webpack dev server.- Add the option for using hot reload in webpack dev server
- Consider switch the typings fro css/sass modules to a ts plugin instead
typescript-plugin-css-modules
- See https://spin.atomicobject.com/2020/06/22/css-module-typescript/ for example
- Actually looks like typescript-plugin-css-modules just helps IDEs provide intellisense as TS doesnt allow plugins to do this