logoMiyauchi

Building a Typescript Environment for Preact with Vite

Introduction

Vite is a build tool developed by Evan You, the author of Vue. It uses native ES Module imports and provide a fast running development environment with no bundling required. Vue3, React and Preact are also supported. In this article, I'll use Vite to build a Preact project environment.

You can find the result template in here.

To do

I will introduce the minimum tools necessary for development. The goal is making it close to the default preact/cli template. The following is a step-by-step explanation of each tool, so that you can introduce them individually.

  • Typescript
  • ESLint
  • Prettier
  • Stylelint
  • husky and lint-staged
  • Path Alias

Building Environments

First, let's expand the vite template.

npm init vite-app <project-name> --template preact
cd <project-name>
npm i

Once the development server is up, you'll be impressed by how fast it is.

Typescript

Then, let's typescript the project. In a minimal configuration, you only need to do two things.

  1. Change all .jsx files to .tsx.
  2. 2.Change the src of the script tag of index.html to /src/main.tsx.

Now you can start up the development server and see that it runs without any problems.

It should work, but I'll add a few more settings to improve the user experience in the editor.

Place the tsconfig.json in your project root. This will tell the editor to recognize the project as a Typescript project.

{
  "compilerOptions": {
    "target": "esnext",
    "lib": ["DOM", "DOM.Iterable", "esnext"],
    "allowJs": false,
    "skipLibCheck": false,
    "esModuleInterop": false,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "preserve",
    "jsxFactory": "h",
    "jsxFragmentFactory": "Fragment"
  },
  "include": ["src"]
}

VSCode shows an error in the .tsx file at this point, so fix it. Add this sentence to all the .tsx files.

import { h } from 'preact'

If you are using Fragment, import it as well.

import { h, Fragment } from 'preact'

Next, fix the entry point, main.tsx. Now that it's in Typescript, a type error has been detected. The document.getElementById returns HTMLElement or null, give it a null check.

You can use the `Non-null assertion operator` if the `app` is always exists in `index.html`.
const el = document.getElementById('app')
if (el) {
  render(<App />, el)
}

Then make some changes to vite.config.js.

const config = {
  jsx: {
    factory: 'h',
    fragment: 'Fragment'
  },
  plugins: [preactRefresh()]
}

export default config

I was able to make Typescript with minimal configuration. You don't have to do the following.

Change vite.config.js to .ts to eliminate .js files. Also, change it to the ES Module format to make the whole project more consistent.

The vite.config.ts should look like this

import preactRefresh from '@prefresh/vite'
import type { UserConfig } from 'vite'

const config: UserConfig = {
  jsx: {
    factory: 'h',
    fragment: 'Fragment',
  },

  plugins: [preactRefresh()],
}

export default config

That's the end of Typescript.

Introducing ESLint

Development without a linter is tough, so be sure to install it.

npm i -D eslint eslint-config-preact @typescript-eslint/parser typescript

{
  "env": {
    "browser": true,
    "es2021": true
  },
  "extends": [
    "eslint:recommended",
    "preact"
  ],
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "ecmaFeatures": {
      "jsx": true
    },
    "ecmaVersion": 12,
    "sourceType": "module"
  },
  "rules": {}
}

It is easy to prepare a linting command in the script of the package.json for later.

It will be easier later on if you have a command for linting in the package.json script of the package.json.

"scripts": {
  "lint:script": "eslint --ext .ts,tsx --ignore-path .gitignore ."
}

Personally, I don't want to fix some situations, so I use --fix from outside.

Now let's run this.

npm run lint:script --fix

VSCode users can also set up the following settings to make the automatic formatting work. An extension to ESLint is required, so if you don't have it, please install it here.

{
  "editor.codeActionsOnSave": {
    "source.fixAll": true
  }
}

This allowed me to format the file on save.

Configuring husky and lint-staged

Before committing, let's run a static check to make sure you can't commit the error code.

npm i -D husky lint-staged

Add the following to package.json.

{
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  },
  "lint-staged": {
    "*.{ts,tsx}": "eslint --fix"
  }
}

This causes ESLint to run against any files with the appropriate extensions in the commit file before you commit.

Of course, on a linting error, the commit is canceled.

Configuring Prettier

Let Prettier do the formatting for your entire project. Also, let Prettier automatically remove semicolons in Typescript code, as they are less visible.

npm i -D prettier eslint-config-prettier

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

When ESLint and Prettier are used together, I need to fix the .eslintrc to avoid duplicate rules.

{
  "extends": [
    "eslint:all",
    "preact",
    // Added under other rules
    "prettier",
    "prettier/@typescript-eslint"
  ]
}

command to execute the formatter.

npm run prettier -w -u .

We want to apply automatic formatting before committing, so we add the setting to lint-staged.

{
 "lint-staged": {
    "*.{ts,tsx}": "eslint --fix",
    "*": "prettier -w -u" // Prettier is the last one to go
  }
}

VSCode users can format it automatically with the following settings. Also, an extension is required, so if it is not available, please install it here.

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

Configuring Stylelint

Let's make the style file a target for linting as well.

npm i -D stylelint stylelint-config-recommended stylelint-config-standard
{
  "extends": ["stylelint-config-recommended", "stylelint-config-standard"]
}

Edit the package.jsoon and set the commands and lint-staged.

{
  "scripts": {
    "lint:style": "stylelint src/**/*.{css,scss}"
  },
  "lint-staged": {
    "*.{ts,tsx}": "eslint --fix",
    "*.{css,scss}": "stylelint --fix",
    "*": "prettier -w -u"
  }
}

VSCode users can format it automatically with the following settings. Extensions are required, so if you don't have them, install them here.

That's the end of the basic setup of the linker and formatter.

Configuring Path Alias

Module import is relative by default, but we want to set alias to always refer to the same root.

Change the vite.config.ts and tsconfig.json to set the alias.

Keys must start with `/`.

import { join } from 'path'
import type { UserConfig } from 'vite'

const config: UserConfig = {
  alias: {
    '/@/': join(__dirname, 'src'),
  }
}
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "/@/*": ["src/*"]
    }
  },
  "include": ["src"]
}

Now you can set up alias. We'll use it like this.

import { App } from '/@/app'

It's a little strange that it has to start from /, but it seems to combine with the alias of the package name. For more information, please refer to here.

That's the minimum environment you can build.