Create a typescript utility library using Vite
Tags: typescript, vite, javascript
Published: 2023-08-15 04:58:53

In this post, we are going to look at how to create a typescript utility library using the [Vite](https://vitejs.dev/) bundler.

## Why?

There could be many reasons to build a library

- To share the code inside your organization
- To contribute to the open-source community by creating an NPM package
- To extract a common piece of code in a mono repo so that multiple apps can reuse the same logic inside the mono repo.

## Technologies/Features used

The library we are going to build will have the following features. Again this is my set of tools feel free to change it based on your preference.

| Feature                   | Technology used |
| ------------------------- | --------------- |
| Package manager           | PNPM            |
| Package bundler           | Vite            |
| Programming language      | Typescript      |
| Basic linting             | ESLint          |
| Code formatting           | Prettier        |
| Pre-commit hook validator | Husky           |
| Linting only staged files | lint-staged     |
| Lint git commit subject   | commitlint      |

## Prerequisites

#### Tools

You will need the following things properly installed on your computer.

- [Git](http://git-scm.com/downloads)
- [Node.js](http://nodejs.org/)

#### PNPM install

- If you have installed the latest v16.x or greater node version in your system, then enable the pnpm using the below cmd

```bash
corepack enable
corepack prepare pnpm@latest --activate
```

- If you are using a lower version of the node in your local system then check this page for additional installation methods https://pnpm.io/installation

## App creation

- Let's create the app by running these commands.

```bash
  pnpm create vite <app-name> --template vanilla-ts
  cd <app-name>
  pnpm install && pnpm update
```

Run `pnpm dev` and check out the app.

- Initialize git if you want and enforce the node version along with some info in the README.md.

```bash
  git init
  npm pkg set engines.node=">=18.16.1" // Use the same node version you installed
  echo "#Typescript utility library" > README.md
```

## Code formatter

I'm going with [Prettier](https://prettier.io/) to format the code. Formatting helps us to keep our code uniform for every developer.

#### Installation

Let's install the plugin and set some defaults. Here I'm setting the single quote to be true, update it according to your preference.

```bash
  pnpm add -D prettier
  echo '{\n  "singleQuote": true\n}' > .prettierrc.json
  echo -e "coverage\npublic\ndist\npnpm-lock.yaml" > .prettierignore
```

#### VS Code plugin

- If you are using VS Code, then navigate to the `Extensions` and search for `Prettier - Code formatter` and install the extension.

Extension link: https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode

- Let's update the workspace to use the prettier as the default formatter and automatically format the file on save.

- Create the workspace settings JSON and update it with the following content.

```bash
mkdir .vscode && touch .vscode/settings.json
```

```json
{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode"
}
```

- Now open the `src/main.ts` in your VSCode editor and save it. If the semicolons are automatically created then it confirms that our auto formatting is working fine.

## Linting

Linter statically analyses your code to quickly find problems. ESLint is the most preferred tool for linting the Javascript code.

### ESLint

```bash
  pnpm create @eslint/config
```

- The ESLint will ask you a set of questions to set up the linter as per your needs.
  This is the configuration I've chosen for this project.

```bash
? How would you like to use ESLint? …
  To check syntax only
❯ To check syntax and find problems
  To check syntax, find problems, and enforce code style

? What type of modules does your project use? …
❯ JavaScript modules (import/export)
  CommonJS (require/exports)
  None of these

? Which framework does your project use? …
  React
  Vue.js
❯ None of these

Does your project use TypeScript? › No / Yes
- Yes

Where does your code run?
✔ Browser
  Node

? What format do you want your config file to be in? …
❯ JavaScript
  YAML
  JSON

The config that you`ve selected requires the following dependencies:
@typescript-eslint/eslint-plugin@latest @typescript-eslint/parser@latest

? Would you like to install them now? › No / Yes
- Yes

? Which package manager do you want to use? …
  npm
  yarn
❯ pnpm
```

- Create the `.eslintignore` file to let the ESLint know which files to not format.

```bash
  touch .eslintignore
  echo -e "coverage\npublic\ndist\npnpm-lock.yaml" > .eslintignore
```

#### Integrating Prettier with ESLint

Linters usually contain not only code quality rules but also stylistic rules. Most stylistic rules are unnecessary when using Prettier, but worse – they might conflict with Prettier!

We are going to use Prettier for code formatting concerns, and linters for code-quality concerns. So let's make the linter run the stylistic rules of 'prettier' instead.

- Install the necessary plugins

```bash
  pnpm add -D eslint-config-prettier eslint-plugin-prettier
```

- Add the `plugin:prettier/recommended` as the **last element** in the extends property in eslintrc.js

```js
module.exports = {
  extends: [..., 'plugin:prettier/recommended'],
}
```

For more info on this: https://prettier.io/docs/en/integrating-with-linters.html

- Let's create scripts for running the linter and prettier in the package.json file.

```bash
  npm pkg set scripts.lint="eslint ."
  npm pkg set scripts.format="prettier --write ."
```

- Run the `pnpm lint` cmd to run the ESLint. If it throws, errors like below then our linter prettier integration is working as expected.

![Image Description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/alb3r51veq19e6dkmk7k.png)

To fix it just run `pnpm format` cmd and run the `pnpm lint --fix` cmd again. Now the errors should be gone.

## Pre-commit hook validation

Even if we added all these linter and formatter mechanisms to maintain the code quality, we can't expect all the developers to use the same editor and execute the `lint` and `format` command whenever they are pushing their code.

To automate that we need some kind of pre-commit hook validation. That's where [husky](https://typicode.github.io/husky/) and [lint-staged](https://www.npmjs.com/package/lint-staged) plugins come in handy let's install and set them up.

- Install the husky, commitlint, and lint-staged NPM package and initialize it as shown below,

```bash
  pnpm add -D @commitlint/cli @commitlint/config-conventional
  echo -e "export default { extends: ['@commitlint/config-conventional'] };" > commitlint.config.js
  pnpm add -D husky lint-staged
  npx husky install
  npx husky add .husky/pre-commit "pnpm lint-staged"
  npx husky add .husky/commit-msg  'npx --no -- commitlint --edit ${1}'
  npm pkg set scripts.prepare="husky install"
```

- Update the package.json file and include the following property. This will run the ESLint on all the script files and Prettier on the other files.

```json
  "lint-staged": {
    "**/*.{js,ts,tsx}": [
      "eslint --fix"
    ],
    "**/*": "prettier --write --ignore-unknown"
  },
```

## Absolute path import support

We often move around the files to make it more meaningful, when we are doing that, it would be great if we don't have to update the import statements. To do that,

- Install the @types/node NPM package

```bash
 pnpm add -D @types/node
```

- Create the vite.config.ts file and update it like this,

```bash
 touch vite.config.ts
```

```js
import { defineConfig } from 'vite'
import { resolve } from 'path'

// https://vitejs.dev/config/
export default defineConfig({
  resolve: { alias: { src: resolve('src/') } },
})
```

The `resolve` property helps us to use absolute import paths instead of relative ones. For example:

```javascript
import { add } from 'src/utils/arithmetic'
```

## App code cleanup:

- Update the `main.ts` file with the following content,

```js
export const add = (a: number, b: number) => a + b
export const sub = (a: number, b: number) => a - b
```

- Delete the sample files

```bash
rm -rf src/style.css src/counter.ts
```

## Vite library mode

Vite by default builds the assets in app mode with index.html as the entry file. But we want our app to expose our main.ts file as the entry file, so let's update the Vite config to support that.

```js
import { defineConfig } from 'vite'
import { resolve } from 'path'

// https://vitejs.dev/config/
export default defineConfig({
  build: { lib: { entry: resolve(__dirname, 'src/main.ts'), formats: ['es'] } },
  resolve: { alias: { src: resolve('src/') } },
})
```

## Local dev server update

We can make use of the local dev server to verify our utility library. To do that let's update the index.html file, do note that it will not be exposed to the outside world.

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite + TS</title>
  </head>
  <body>
    <script type="module" src="/src/main.ts"></script>
    <script type="module">
      import { add, sub } from './src/main'

      const addOutput = document.getElementById('addOutput')
      const subOutput = document.getElementById('subOutput')
      const input1 = document.getElementById('input1')
      const input2 = document.getElementById('input2')
      const getNumber = (element) => parseInt(element?.value)
      const eventHandler = () => {
        const firstVal = getNumber(input1)
        const secondVal = getNumber(input2)
        if (addOutput && subOutput) {
          addOutput.innerText = add(firstVal, secondVal).toString()
          subOutput.innerText = sub(firstVal, secondVal).toString()
        }
      }
      eventHandler()
      input1?.addEventListener('change', eventHandler)
      input2?.addEventListener('change', eventHandler)
    </script>

    <input id="input1" type="number" value="4" />
    <input id="input2" type="number" value="3" />
    <p>Sum of two numbers = <span id="addOutput"></span></p>
    <p>Diff of two numbers = <span id="subOutput"></span></p>
  </body>
</html>
```

Run the dev server, you should be able to see the library methods in action.

## Unit testing

We are going to use the most popular unit testing framework [Jest](https://jestjs.io/) for writing our unit tests.

- Install the necessary packages

```bash
pnpm add -D jest @types/jest ts-jest ts-node
```

| Package | Purpose |
| ----------------------------------------------- | --------------------------------------------- | |
| jest | Test runner |
| ts-jest | For running jest in typescript supported app. |
| ts-node | For creating jest config file in TS extension |
| @types/jest | Types library for Jest |

- Create the `src/setupTests.ts` file and update it with the below content.

```bash
 touch src/setupTests.ts
```

```js
beforeAll(() => {
  // Add your global beforeAll logics
})

beforeEach(() => {
  // Add your globalbeforeEach logics
})

afterAll(() => {
  // Add your global afterAll logics
})

afterEach(() => {
  // Add your global afterEach logics
})
```

- Create the jest.config.ts file and update it with the below config.

```bash
 touch jest.config.ts
```

```javascript
export default {
  preset: 'ts-jest',
  testEnvironment: 'node',
  moduleNameMapper: {
    '^src/(.*)$': '<rootDir>/src/$1',
  },
  setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
  modulePathIgnorePatterns: ['./dist/'],
  coveragePathIgnorePatterns: [],
  collectCoverageFrom: ['./src/**/*.ts', '!./src/**/*.test.ts'],
  coverageThreshold: {
    global: {
      branches: 100,
      functions: 100,
      lines: 100,
      statements: 100,
    },
  },
}
```

- Create the jest.ci-config.ts file and update it with the below config.

```bash
 touch jest.ci-config.ts
```

```js
import config from './jest.config'

export default {
  ...config,
  coverageThreshold: {},
}
```

- Create a test file for `main` component and update it like the below,

```bash
touch src/main.test.ts
```

```js
import { add, sub } from './main'

describe('Utility | Main', () => {
  it('add - should add the given two numbers', async () => {
    expect(add(4, 2)).toEqual(6)
  })

  it('sub - should subtract the given two numbers', async () => {
    expect(sub(4, 2)).toEqual(2)
  })
})
```

- Update the package scripts for running tests

```bash
npm pkg set scripts.test="jest"
npm pkg set scripts.test:coverage="jest --coverage"
npm pkg set scripts.test:ci="jest --coverage --config='./jest.ci-config.ts'"
```

- Run the tests

```bash
  pnpm test
```

- To run a particular test use the -t param.

```bash
  pnpm test -- -t "add"
```

## Sample repo

The code for this post is hosted in Github [here](https://github.com/vinomanick/ts-library).

Please take a look at the Github repo and let me know your feedback, and queries in the comments section.