Build a Vue 3 + TypeScript dev environment with Vite

Production grade DX for all your web projects.

2021-08-22
2023-09-11
frontend
vue 3
typescript
vite
tailwindcss

Introduction

This is an opinionated guide on how to set up a new Vue 3 project with the aforementioned tech stack plus some extras that can really help in the DX (Developer Experience) department.

Stuff like:

  • Prettier
  • Husky
  • ESLint + styleLint + commitlint
  • Maybe Tailwind CSS ;)

And more…

Here are a couple of cheat sheets I wrote for Vue 3 + TypeScript:

Prerequisites

Let’s get started

Generate a new project:

pnpm create vite

Then input:

  • Project name
  • Pick Vue
  • Pick TypeScript

Let’s set up our project with pnpm.
Enter the project directory, then:

pnpm install
pnpm up --latest

If you see an error inside VSCode that reads:

Cannot find module './App.vue' or its corresponding type declarations

You’ll need to install the Volar VSCode extension, then you’ll need to enable Takeover Mode to get proper TS support for Vue components:

Volar Takeover Mode

Basic project configuration

Create a prettier.config.cjs file and put this content inside:

/** @type {import("prettier").Config} */
module.exports = {
  semi: false,
  singleQuote: true,
}

This is in case you have the VSCode extension installed, and you want to format code right away. ;)

We will finish setting it up later.

common node types

To be able to use __dirname or import * as path from 'path':

pnpm add -D @types/node

Typed ENV vars

To type your environment variables in Vite, add a new src/env.d.ts file with typing information like:

interface ImportMetaEnv {
  VITE_BASE_URL: string
}

This indicates that we will have a VITE_BASE_URL env var that will contain a string.

alias @ to src

Use @ in imports like import HelloWorld from '@/components/HelloWorld.vue'.

Edit vite.config.ts and add a new resolve key to it, like this:

import vue from '@vitejs/plugin-vue'
import * as path from 'path'
import { defineConfig } from 'vite'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
})

Let’s allow TypeScript to resolve this alias, and also enable autocomplete on paths.

In tsconfig.json add paths and baseUrl under compilerOptions:

{
  "compilerOptions": {
    // ...

    /* Alias resolving */
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }

As to why "skipLibCheck": true, here is some context about how it might be useful.

Improve your DX

DX means Developer Experience

— lt

These dependencies are optional, and if you are just starting might want to skip them altogether.

Their aim is to set up an advanced DX for web development, and are essential when working in the context of a team.

prettier + Tailwind CSS plugin

Add Prettier to the project:

pnpm add -D prettier prettier-plugin-tailwindcss

Create a prettier.config.cjs file:

/** @type {import("prettier").Config} */
module.exports = {
  plugins: ['prettier-plugin-tailwindcss'],
  semi: false,
  singleQuote: true,
  // trailingComma: "none" || "es5"
  // tailwindConfig: './tailwind.config.js'
}

Create a .prettierignore file:

coverage/
dist/
__snapshots__/
pnpm-lock.yaml
*.md

Format project:

pnpm prettier . --write

Add the following scripts to your package.json file:

{
  // ...
  "scripts": {
    // ...
    "format": "prettier . --write",
    "format:check": "prettier . --check",
    // ...

Read more about Prettier over here.
And read more about the Tailwind CSS plugin over here.

ESLint

Let’s configure eslint + prettier + typescript support.
Add eslint and related dependencies to the project:

pnpm add -D eslint eslint-plugin-vue eslint-config-prettier
pnpm add -D vue-eslint-parser @typescript-eslint/parser
pnpm add -D @typescript-eslint/eslint-plugin

Create a .eslintrc.cjs file:

module.exports = {
  env: { node: true },
  extends: [
    'eslint:recommended', // this maybe causes errors in defineEmits<{}>() ???
    'plugin:@typescript-eslint/recommended',
    'plugin:vue/vue3-recommended',
    'prettier',
  ],
  /* globals: {
    defineEmits: 'readonly',
    defineProps: 'readonly',
  }, */
  parser: 'vue-eslint-parser',
  parserOptions: {
    parser: '@typescript-eslint/parser',
    sourceType: 'module',
  },
  // plugins: ['@typescript-eslint'], // might not be needed
  /* rules: {
    '@typescript-eslint/explicit-function-return-type': 'warn',
  }, */
}

Add the following scripts to your package.json file:

{
  // ...
  "scripts": {
    // ...
    "js:lint": "eslint --ext .js,.ts,.vue --ignore-path .gitignore --fix src/",
    "js:lint:check": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src/",
    // ...

Read more about eslint-plugin-vue over here.

Stylelint

Add Stylelint to the project:

pnpm add -D stylelint stylelint-config-standard stylelint-config-prettier stylelint-config-recommended-vue postcss-html

Create a stylelint.config.cjs:

module.exports = {
  extends: [
    'stylelint-config-standard',
    'stylelint-config-recommended-vue',
    'stylelint-config-prettier'
  ],
  rules: {
    'at-rule-no-unknown': [true, { ignoreAtRules: ['tailwind'] }],
    'declaration-empty-line-before': null,
    'function-no-unknown': [true, { ignoreFunctions: ['theme'] }],
    'custom-property-empty-line-before': null,
    'selector-class-pattern': null,
    'value-keyword-case': null,
  }
}

Read more about Stylelint over here.

Add the following scripts to your package.json file:

{
  // ...
  "scripts": {
    // ...
    "css:lint": "stylelint ./src/**/*.{css,vue} --fix",
    "css:lint:check": "stylelint ./src/**/*.{css,vue}",
    // ...

commitlint

Add commitlint to the project:

pnpm add -D @commitlint/cli @commitlint/config-conventional

Create a commitlint.config.cjs:

module.exports = {
  extends: ['@commitlint/config-conventional'],
}

Read more about commitlint over here.

husky

Add husky to the project:

pnpm add -D husky
pnpm husky install
npm pkg set scripts.prepare="husky install"
pnpm husky add .husky/commit-msg 'pnpm commitlint --edit $1'
pnpm husky add .husky/pre-commit 'pnpm format:check'
pnpm husky add .husky/pre-commit 'pnpm js:lint:check'
pnpm husky add .husky/pre-commit 'pnpm css:lint:check'

Whenever we clone a repository that contains husky git hooks we need to run pnpm husky install —or, in this case pnpm prepare— for husky to set them up.

Common project dependencies

These are some really common dependencies that you’ll see basically in every project.

vue-router

Add vue-router to the project:

pnpm add vue-router

Set up router with example:

Add a src/pages/HomePage.vue file:

<template>Home page</template>

Add a src/router.ts file:

import { createRouter, createWebHistory } from 'vue-router'

import HomePage from '@/pages/HomePage.vue'

export const routes = [
  {
    path: '/',
    name: 'home-page',
    component: HomePage,
  },
  // For lazy loading components
  /* {
    path: '/',
    name: 'home-page',
    component: () => import('@/pages/HomePage.vue'),
  }, */
]

export const router = createRouter({
  history: createWebHistory(),
  routes,
})

Adjust your src/main.ts file:

import { createApp } from 'vue'

import './style.css'
import App from './App.vue'
import { router } from './router'

createApp(App).use(router).mount('#app')

Add a router outlet in you src/App.vue file:

<template>
  <div>
    <a href="https://vitejs.dev" target="_blank">
      <img src="/vite.svg" class="logo" alt="Vite logo" />
    </a>
    <a href="https://vuejs.org/" target="_blank">
      <img src="./assets/vue.svg" class="logo vue" alt="Vue logo" />
    </a>
  </div>
  <HelloWorld msg="Vite + Vue" />

  <!-- ROUTER OUTLET -->
  <RouterView />
</template>

Adding a not found page to our router

Now let’s add a catch all route that’ll render a special 404 page.

Create a new NotFoundPage.vue, e.g.:

<template>404 - Not found</template>

Then add a new entry to your routes in the src/router.ts file:

import NotFoundPage from '@/pages/NotFoundPage.vue'

export const routes = [
  // ...
  {
    component: NotFoundPage,
    name: 'not-found-page',
    path: '/:pathMatch(.*)*',
  },
]

Type router meta variables

Configure your router variables in src/vue-router.d.ts:

export {}

declare module 'vue-router' {
  interface RouteMeta {
    authRequired?: boolean
    unauthRequired?: boolean
  }
}

Read more about it:

Other recommended packages/libraries

date-fns

A modern JavaScript date utility library.
Makes it easy to work with timezones.

pnpm add date-fns date-fns-tz

VueUse

The swiss army knife for Vue 3.
A collection of essential Vue composition utilities.

pnpm add @vueuse/core

Lodash

The swiss army knife for JavaScript/TypeScript.
A modern JavaScript utility library delivering modularity, performance & extras.

pnpm add lodash-es
pnpm add -D @types/lodash-es

Vuelidate

A simple, but powerful, lightweight model-based validation for Vue.

pnpm add @vuelidate/core @vuelidate/validators

Axios

Promise based HTTP client for the browser and node.js

pnpm add axios

Big.js

A small, fast JavaScript library for arbitrary-precision decimal arithmetic.

pnpm add big.js
pnpm add -D @types/big.js

Histoire

Write stories to showcase and document your components.

pnpm i -D histoire @histoire/plugin-vue

Add a histoire.config.ts file:

import { defineConfig } from 'histoire'
import { HstVue } from '@histoire/plugin-vue'

export default defineConfig({
  plugins: [HstVue()],
  setupFile: 'src/histoire.setup.ts',

  // If you intend to serve from a subdirectory...
  vite: {
    base: '/ui-library/',
  },
})

Create a src/histoire.setup.ts:

import { createPinia } from 'pinia'
import { defineSetupVue3 } from '@histoire/plugin-vue'

import './style.css'

export const setupVue3 = defineSetupVue3(({ app, story, variant }) => {
  const pinia = createPinia()
  app.use(pinia)
})

Then add these scripts to your package.json:

{
  "scripts": {
    "story:dev": "histoire dev",
    "story:build": "histoire build",
    "story:preview": "histoire preview"
  }
}

Create an env.d.ts:

/// <reference types="@histoire/plugin-vue/components" />

And add it in the include field of your tsconfig.json:

{
  "include": [
    "env.d.ts",
    "src/**/*.ts",
    "src/**/*.d.ts",
    "src/**/*.tsx",
    "src/**/*.vue"
  ]
}

Let’s write an example story file for the src/components/HelloWorld.vue component.

src/components/HelloWorld.story.vue:

<script setup lang="ts">
import HelloWorld from './HelloWorld.vue'
</script>

<template>
  <Story>
    <HelloWorld msg="Hello!" />
  </Story>
</template>

To see the results:

pnpm story:dev
# Now visit http://localhost:6006

:tada:

References