Notes
Setting up React Project with TypeScript, ESBuild, ESLint and Lite Server
A complete walkthrough on the process of wiring different tools and technologies into a working React app

React projects can be scaffolded in many different ways, predominantly by using Facebook's Create React App or Vercel's Next.js.

However, Create React App is notorious for being unscalable. Enterprise-level apps that uses CRA will inevitably forced to invoke the much dreaded react-scripts eject command that unfolds the nicely abstracted curtains and exposes all the underlyings of the setup so that further customizations can be made. Nonetheless, it is an absolutely daunting task to take on.

Next.js is great but I want a vanilla React project to work with, so I decided to create one on my own. Here is the tutorial on how I wired things up and get it up and running.

First, we will need a package.json file and it can be generated by using NPM. The -y flag defaults all the prompt and generate the package.json that can be worked on immediately.

npm init -y

Create a src folder, and a public folder on the root. Install React and React-DOM accordingly.

pnpm i react react-dom

Add the typing for React and TypeScript as dev dependencies.

pnpm i -D @types/react @types/react-dom typescript

Create a tsconfig.json with the following configurations.

{
  "compilerOptions": {
    "lib": ["es6", "dom"],
    "allowJs": false,
    "jsx": "react",
    "esModuleInterop": true,
    "noImplicitAny": true,
    "strictNullChecks": true
  },
  "exclude": ["node_modules", "dist", "public"]
}

Create an entry point for the application in src folder named App.tsx and populate with the following contents. What this does is just inject the <h1>Hello world</h1> into a blank html page.

import React from 'react'
import ReactDOM from 'react-dom/client'

const App = () => <h1>Hello world</h1>

const root = ReactDOM.createRoot(document.getElementById('root')!)
root.render(<App />)

Next, create the blank index.html file inside the public folder with the following contents.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>React ESBuild</title>
  </head>
  <body>
    <div id="root"></div>

    <script src="/dist/bundle.js"></script>
  </body>
</html>

ESBuild

pnpm i -D esbuild

ESBuild can be executed via the ESBuild executable or Node.js with a ESBuild config file. The example command for the executable looks like this:

esbuild src/App.tsx --bundle --minify --sourcemap --outfile=public/dist/bundle.js

Running the command will gives us the output as below.

  public\dist\bundle.js      139.1kb
  public\dist\bundle.js.map  340.2kb

Done in 35ms

However, we will be using the config file version because it is cleaner and offers more customization in my opinion.

Create the esbuild.config.js with the following contents. What this does is just compile the code starting from the entry point specified and output the bundle files into public/dist/bundle.js. It is also set in a watch mode so that rebuild is triggered each time when there is changes to the files that it is listening to.

The onRebuild function is called each time a rebuild is triggered, and it is the great place to logs the status of the rebuild.

require('esbuild')
  .build({
    entryPoints: ['src/App.tsx'],
    bundle: true,
    minify: false,
    format: 'cjs',
    sourcemap: false,
    outfile: 'public/dist/bundle.js',
    watch: {
      onRebuild(error, result) {
        var now = new Date()
        if (error) {
          console.log(
            '🙈\x1b[2m %s: \x1b[0m\x1b[37m\x1b[41m%s\x1b[0m %s',
            now.toTimeString(),
            'FAILURE',
            error.message
          )
        } else {
          console.log(
            '🐻‍❄️\x1b[2m %s: \x1b[0m\x1b[30m\x1b[42m%s\x1b[0m %s',
            now.toTimeString(),
            'COMPLETE',
            'Build successful'
          )
        }
      },
    },
  })
  .then(() => console.log('watching...'))
  .catch(() => process.exit(1))

After that, add the run script to package.json for both the watch version and the build version of the ESBuild.

{
  "scripts": {
    "build": "esbuild src/App.tsx --bundle --minify --sourcemap --outfile=public/dist/bundle.js",
    "watch": "node esbuild.config.js"
  }
}

Lite Server

Next, we will need to have a server that can host the files and listen to changes. We will use lite server for this.

pnpm i -D lite-server

Create a bs-config.js file to store configurations related to lite server. The only settings we need is setting the base directory of the server to the public folder where the index.html resides.

module.exports = {
  server: {
    baseDir: './public',
  },
}

Add a run script in package.json to start the server.

"start": "lite-server"

Running the script with pnpm start and you will see the output from lite server as such.

[Browsersync] Access URLs:
 ---------------------------------------
       Local: http://localhost:3000
    External: http://192.168.68.109:3000
 ---------------------------------------
          UI: http://localhost:3001
 UI External: http://localhost:3001
 ---------------------------------------
[Browsersync] Serving files from: ./
[Browsersync] Watching files...

ESLint

To generate a ESLint config file, run the following command.

npm init @eslint/config

A command dialog will prompt for the preferred options to use with ESLint and select accordingly (not necessary follow my choice).

npx: installed 41 in 5.263s
√ How would you like to use ESLint? · style
√ What type of modules does your project use? · esm
√ Which framework does your project use? · react
√ Does your project use TypeScript? · Yes
√ Where does your code run? · browser
√ How would you like to define a style for your project? · guide
√ Which style guide do you want to follow? · standard-with-typescript
√ What format do you want your config file to be in? · YAML
Checking peerDependencies of eslint-config-standard-with-typescript@latest
Local ESLint installation not found.
The config that you've selected requires the following dependencies:

eslint-plugin-react@latest eslint-config-standard-with-typescript@latest @typescript-eslint/eslint-plugin@^5.0.0 eslint@^8.0.1 eslint-plugin-import@^2.25.2 eslint-plugin-n@^15.0.0 eslint-plugin-promise@^6.0.0 typescript@*
√ Would you like to install them now? · No
Successfully created .eslintrc.yml file in C:\Users\my-project

Select No when prompted to install the dependencies as I presume that it will use NPM to install and generate a package.lock.json file which in my case I am using Pnpm. Install the dependencies separately.

pnpm i -D eslint-plugin-react@latest eslint-config-standard-with-typescript@latest @typescript-eslint/eslint-plugin@^5.0.0 eslint@^8.0.1 eslint-plugin-import@^2.25.2 eslint-plugin-n@^15.0.0 eslint-plugin-promise@^6.0.0 @typescript-eslint/parser

One more step to silence the warning that says unspecified React version is to append this at the bottom of .eslintrc.yml.

settings:
  react:
    version: 'detect'

After that add the following run script into package.json.

"lint": "eslint src/**/*.tsx"

The run script will lint all the files that end with tsx within the src folder. Running ESLint is as easy as

pnpm lint

And the example output for ESLint that contains error are as shown.

C:\Users\MyProject\src\App.tsx
   1:19  error  Strings must use singlequote     @typescript-eslint/quotes
   1:26  error  Extra semicolon                  @typescript-eslint/semi
   2:22  error  Strings must use singlequote     @typescript-eslint/quotes
   2:40  error  Extra semicolon                  @typescript-eslint/semi
   4:24  error  Strings must use singlequote     @typescript-eslint/quotes
   4:45  error  Extra semicolon                  @typescript-eslint/semi
   6:13  error  Missing return type on function  @typescript-eslint/explicit-function-return-type
  11:2   error  Extra semicolon                  @typescript-eslint/semi
  13:34  error  Forbidden non-null assertion     @typescript-eslint/no-non-null-assertion
  13:58  error  Strings must use singlequote     @typescript-eslint/quotes
  13:67  error  Extra semicolon                  @typescript-eslint/semi
  14:21  error  Extra semicolon                  @typescript-eslint/semi

✖ 21 problems (21 errors, 0 warnings)
  18 errors and 0 warnings potentially fixable with the `--fix` option.

Starting the App

To start the app, we will need two separate terminals. The first terminal listens to the changes in the React files and compile them when new changes are made.

pnpm watch

The second is running the lite server to serve the app locally.

pnpm start

Up Next

The next step is to setup a testing framework as well as using Tailwind for styling. Setting up the testing framework with @testing-framework/react and Jest on my own is absolutely painful as the toolchains are convoluted and confusing for me that had always taken granted for the out-of-the-box and low-setup tests settings while using Create React App or Next.js.

I had spent countless hours debugging the issues faced, installing and uninstalling packages and meddling round with tonnes of config files and fortunately able to make everything works. I think that setting up tests in this project deserves a separate article on its own, so stay tuned.

References