Wiring up a TypeScript environment with Preact, Vite and Vitest and vitest-dom

I have heard good things about Vite and Vitest. When I gave them a test-drive, I stumbled over some minor annoyances in getting the whole suite running.

I’m writing down the steps I took, maybe they help you.

The article is basically a re-write of a blog post by Tomoki Miyaci adjusted to my needs.

Tooling:

  • TypeScript
  • Vite
  • Vitest (with vitest-dom)
  • Preact
  • Prettier
  • ESLint
  • husky & lint-staged
  • commitlint

All my commands use pnpm, feel free to replace them with npm or yarn.

Vite

pnpm create vite <project-name> --template preact-ts
cd <project-name>
pnpm install

ESLint & prettier

pnpm i -D eslint eslint-config-prettier \
          prettier \
          @typescript-eslint/parser

Create a new file called .eslintrc with the following content:

{
  "env": {
    "browser": true,
    "es2021": true
  },
  "extends": ["eslint:recommended", "preact", "prettier"],
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "ecmaFeatures": {
      "jsx": true
    },
    "ecmaVersion": "latest",
    "sourceType": "module"
  },
  "settings": {
    "jest": {
      "version": 27
    }
  },
  "ignorePatterns": ["*.d.ts"],
  "rules": {}
}

One wrinkle was the Jest settings option. I know I wanted to use Vite, but eslint needs to know the Jest version for some of its tests.

Prettier configuration (.prettierrc), adjust to your needs:

{
  "trailingComma": "es5",
  "semi": false,
  "singleQuote": true
}

Let’s adjust package.json:

{
  "scripts": {
    "lint:fix": "eslint --fix --ext .ts,tsx --ignore-path .gitignore .",
    "prettier:write": "prettier -u -w --ignore-path .gitignore \"*.{ts,tsx,css,html}\"",
}

husky & lint-staged

Configure lint-staged inside package.json:

{
  "lint-staged": {
    "*.{ts,tsx}": ["pnpm run lint:fix", "pnpm run prettier:write"],
    "*.{html,css,js,json}": "pnpm run prettier:write"
  }
}

We run ESLint and prettier on TypeScript files in sequential order. For other files, prettier suffices.

Now we need husky.

We initialize it with a script:

pnpm dlx husky-init && pnpm install

The above command will setup the tool and create the necessary files and hooks.

The default hook runs before committing the files to the staging area.
You can find it under .husky/pre-commit:

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

pnpm exec lint-staged
## if you want to run your tests before commiting,
## uncomment next line
# pnpm exec vitest run

commitlint

commitlint checks if your commit messages meet the conventional commit format.

Example:

chore: run tests on travis ci

I personally find it quite useful to enforce a uniform commit style.
commitlint pairs well with husky.

pnpm add -D @commitlint/{config-conventional,cli}

Let’s add a configuration file (.commitlintrc.json):

{
  "extends": ["@commitlint/config-conventional"]
}

Now we need a hook for husky. Run the following command in your terminal:

pnpm dlx husky add \
  .husky/commit-msg 'pnpm exec commitlint --edit'

Vitest

Installation:

pnpm add -D vitest vitest-dom happy-dom

vitest-dom extends the standard Jest matchers with convenient methods like .toBeDisabled.
Now you can write tests that assert on the state of the DOM.
The package is a fork of @testing-library/jest-dom.

Configuring vitest with the .vite.config.ts:

/// <reference types="vitest" />
import { fileURLToPath } from 'url'
import { defineConfig } from 'vite'
import preact from '@preact/preset-vite'

// https://vitejs.dev/config/
export default defineConfig({
  define: {
    'import.meta.vitest': 'undefined',
  },
  plugins: [preact()],
  test: {
    environment: 'happy-dom',
    setupFiles: ['./__test__/test-setup.ts'],
    includeSource: ['src/**/*.{ts,tsx}'],
    coverage: {
      reporter: ['text-summary', 'text'],
    },
    mockReset: true,
    restoreMocks: true,
  },
})

The code section import.meta.vitest allows you to run tests within your source code.

For my test setup I’ve made a separate __test__ folder with a file called test-setup.ts:

import 'vitest-dom/extend-expect'
import * as domMatchers from 'vitest-dom/matchers'
import { expect } from 'vitest'

expect.extend(domMatchers)

Here I add the vitest-dom extra matchers. You can add more setup logic if needed.

Sources