Skip to content
This repository has been archived by the owner on Nov 12, 2022. It is now read-only.

lski/webpack-babel-typescript-react-template

Repository files navigation

Webpack/Babel/Typescript/(P)react Template

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.

Out the Box Features

  • Code splitting (provided by webpack) when dynamic imports import() is used
  • Using a .env file to set your environment variables.
  • importing 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.

Setup

  • Get code

    # Clone
    git clone git@github.com: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 :)

Scripts

To run the application the scripts are similar to those of Create React App.

  • yarn run start Starts a development build using webpack-dev-server

  • yarn run test Runs tests

  • yarn run build Creates a minified production ready build

  • yarn run build:dev Similar to build 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 by yarn 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.

Settings

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.

Environment variables:

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.

Custom Environment Variables

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: WPT_APP_ADMIN_EMAIL=joe.bloggs@email.com could be referenced directly in code with process.env.WPT_APP_ADMIN_EMAIL.

Types

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.

Docker

This application supports both developing your application in a Docker container and also running a production version.

For Development

# 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)

Run Production

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!

Extending Features

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.
  • 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 emotions jsx() function rather than react (or preact) version of jsx() to support the css 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.

Roadmap

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

About

A Template Build Pipeline for Webpack and Typescript/Babel for creating a (P)React SPA

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published