DEV Community

Vinodh Kumar
Vinodh Kumar

Posted on • Updated on

Create a typescript utility library using Vite

In this post, we are going to look at how to create a typescript utility library using the Vite 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.

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
corepack enable
corepack prepare pnpm@latest --activate
Enter fullscreen mode Exit fullscreen mode
  • 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.
  pnpm create vite <app-name> --template vanilla-ts
  cd <app-name>
  pnpm install && pnpm update
Enter fullscreen mode Exit fullscreen mode

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.
  git init
  echo -e "node_modules\ncoverage" > .gitignore
  npm pkg set engines.node=">=22.11.0" // Use the same node version you installed
  echo "#Typescript utility library" > README.md
Enter fullscreen mode Exit fullscreen mode
  • Specify the latest PNPM version to use for this project by setting the packageManager property in the package.json file.
  npm pkg set packageManager="pnpm@9.12.3"
  pnpm -v
Enter fullscreen mode Exit fullscreen mode

Code formatter

I'm going with Prettier 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.

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

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.

mkdir .vscode && touch .vscode/settings.json
Enter fullscreen mode Exit fullscreen mode
{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode"
}
Enter fullscreen mode Exit fullscreen mode
  • 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

  pnpm create @eslint/config@latest
Enter fullscreen mode Exit fullscreen mode
  • 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.
? How would you like to use ESLint? …
  To check syntax only
❯ To check syntax and find problems

? 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

The config that you`ve selected requires the following dependencies:
eslint, globals, @eslint/js, typescript-eslint

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

? Which package manager do you want to use? …
  npm
  yarn
❯ pnpm
  bun
Enter fullscreen mode Exit fullscreen mode
  • Update the eslint config with the ignores list to let the ESLint know which files to not format.
/** @type {import('eslint').Linter.Config[]} */
export default [
  ...
  {
    // Note: there should be no other properties in this object
    ignores: ['coverage', 'public', 'dist', 'pnpm-lock.yaml'],
  },
  ...
]
Enter fullscreen mode Exit fullscreen mode

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
  pnpm add -D eslint-config-prettier eslint-plugin-prettier
Enter fullscreen mode Exit fullscreen mode
  • Add the eslintPluginPrettierRecommended as the last element in the array.
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'

/** @type {import('eslint').Linter.Config[]} */
export default [
  ...,
  eslintPluginPrettierRecommended,
]
Enter fullscreen mode Exit fullscreen mode

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.
  npm pkg set scripts.lint="eslint ."
  npm pkg set scripts.format="prettier --write ."
Enter fullscreen mode Exit fullscreen mode
  • 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

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 and 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,
  pnpm add -D @commitlint/cli @commitlint/config-conventional
  echo -e "export default { extends: ['@commitlint/config-conventional'] };" > commitlint.config.mjs
  pnpm add -D husky lint-staged
  pnpm exec husky init
  echo "pnpm lint-staged" > .husky/pre-commit
  echo "npx --no -- commitlint --edit \${1}" > .husky/commit-msg
Enter fullscreen mode Exit fullscreen mode
  • 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.
  "lint-staged": {
    "**/*.{js,ts}": [
      "eslint --fix"
    ],
    "**/*": "prettier --write --ignore-unknown"
  },
Enter fullscreen mode Exit fullscreen mode

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
 pnpm add -D @types/node
Enter fullscreen mode Exit fullscreen mode
  • Create the vite.config.ts file and update it like this,
 touch vite.config.ts
Enter fullscreen mode Exit fullscreen mode
import { defineConfig } from 'vite'
import { resolve } from 'path'

// https://vitejs.dev/config/
export default defineConfig({
  resolve: { alias: { src: resolve('src/') } },
})
Enter fullscreen mode Exit fullscreen mode

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

import { add } from 'src/utils/arithmetic'
Enter fullscreen mode Exit fullscreen mode

App code cleanup:

  • Update the main.ts file with the following content,
export const add = (a: number, b: number) => a + b
export const sub = (a: number, b: number) => a - b
Enter fullscreen mode Exit fullscreen mode
  • Delete the sample files
rm -rf src/style.css src/counter.ts
Enter fullscreen mode Exit fullscreen mode

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.

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/') } },
})
Enter fullscreen mode Exit fullscreen mode

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.

<!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>
Enter fullscreen mode Exit fullscreen mode

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

Unit testing

Vitest is a new framework that is gaining popularity with the developers who are using Vite. Let's see how to configure it for our project.

  • Install the necessary packages
pnpm add -D vitest @vitest/coverage-v8
Enter fullscreen mode Exit fullscreen mode
  • Create a test file for main component and update it like the below,
touch src/main.test.ts
Enter fullscreen mode Exit fullscreen mode
import { add, sub } from './main'
import { describe, expect, test } from 'vitest'

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

  test('sub - should subtract the given two numbers', async () => {
    expect(sub(4, 2)).toEqual(2)
  })
})
Enter fullscreen mode Exit fullscreen mode
  • Update the package scripts for running tests
npm pkg set scripts.test="vitest"
npm pkg set scripts.test:cov="vitest run --coverage"
Enter fullscreen mode Exit fullscreen mode
  • Create the src/setupTests.ts file and update it with the below content.
 touch src/setupTests.ts
Enter fullscreen mode Exit fullscreen mode
import { beforeAll, beforeEach, afterAll, afterEach } from 'vitest'

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

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

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

afterEach(() => {
  // Add your global afterEach logics
})
Enter fullscreen mode Exit fullscreen mode
  • Update the vite.config.ts file to add the test configs.
import { defineConfig } from 'vitest/config';

// https://vitejs.dev/config/
export default defineConfig({
  ...,
  test: {
    setupFiles: ['src/setupTests.ts'],
    coverage: {
      exclude: ['*.config.*', '*.d.ts'],
    },
  },
});
Enter fullscreen mode Exit fullscreen mode
  • Run the tests
  pnpm test
Enter fullscreen mode Exit fullscreen mode
  • Run the tests with coverage
  pnpm test:cov
Enter fullscreen mode Exit fullscreen mode
  • To run a particular test use the -t param.
  pnpm test -- -t "add"
Enter fullscreen mode Exit fullscreen mode

Sample repo

The code for this post is hosted in Github here.

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

Top comments (9)

Collapse
 
nishchit14 profile image
Nishchit

Great writing, Thanks for the detaled article. Would you mind to add readme description in GitHub repo so one can get to know where to start.

Collapse
 
vinomanick profile image
Vinodh Kumar

Thanks for the feedback. Have added the readme as suggested please check it out.

Collapse
 
nishchit14 profile image
Nishchit

Perfect, Thank you.

Collapse
 
zhamdi profile image
Zied Hamdi

This is cool, but the output is a single file, I'd like to compile multiple ts files as tsc does. Is that possible with vite?

I'm new to all of this, so sorry if it's a dumb question.

Collapse
 
vinomanick profile image
Vinodh Kumar • Edited

entry: [
resolve(__dirname, 'src/main.ts'),
resolve(__dirname, 'src/main-2.ts'),
],

Yes it's possible, update the build.lib.entry from type string to array in the vite.config.ts file and add all your input ts files in that array. It will compile and output multiple ts files. Hope that answers your question.

For more info checkout:
vitejs.dev/guide/build.html#librar...
vitejs.dev/config/build-options.ht...

Collapse
 
clabnet profile image
Claudio Barca

Thanks for this guide.
Why not use Vitest? It seems very integrated with Vite.

Collapse
 
vinomanick profile image
Vinodh Kumar

Hey thanks. I first started using Vite for a react project and went with Jest as it was the recommended framework of choice for the react app. And also, Vitest was still in early stage so went with the more popular testing framework and got used to the Jest setup.
Vitest looks promising and the APIs are also lot similar to Jest so will definitely give a try.

Collapse
 
manchicken profile image
Mike Stemle

I do wish that this covered more of how Vite works. I don’t feel like I understand how Vite interacts with my code based on this write-up.

Collapse
 
clabnet profile image
Claudio Barca

In a few words: with Vite you can build web app (Vue, React, etc.) and libraries containing shared common code between projects.
This post explain how to create a library with Vite as bundler.