Setting Up A Modern Webdev Toolchain with Vite

Why?

We often want to organize our code into separate files and make use of third-party packages. A bundler takes all of your code and its dependencies and produces optimized output files for the browser.

We also want to write TypeScript for type safety, use JSX for React components, and transform our CSS with PostCSS — all of which require some kind of build step.

Vite handles all of this. It provides a lightning-fast dev server with hot module replacement, and uses Rolldown — a Rust-based bundler — under the hood for both development and production builds. Unlike older tools like Webpack, Vite requires almost no configuration to get started.

Getting started

Let's create a project from scratch:

terminal
mkdir my-app && cd my-app
npm init -y
npm i -D vite@npm:rolldown-vite typescript

Vite uses index.html as its entry point — unlike Webpack which started from a JavaScript file. Create one in the project root:

index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

And a simple TypeScript file:

src/main.ts
document.querySelector('#root')!.innerHTML = '<h1>Hello world!</h1>'

Now start the dev server:

terminal
npx vite

Open http://localhost:5173 and you should see "Hello world!" — Vite handles TypeScript out of the box, no configuration needed.

Try editing src/main.ts and saving — the page updates instantly. Vite's dev server uses native ES modules and only transforms files on demand, which is why it starts up so fast regardless of project size.

Adding React

terminal
npm i react react-dom
npm i -D @vitejs/plugin-react @types/react @types/react-dom

Create a Vite config to enable the React plugin:

vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
})

Now replace src/main.ts with a React entry point, and update the script tag in index.html to match:

import { createRoot } from 'react-dom/client'
import { App } from './App'
createRoot(document.querySelector('#root')!).render(<App />)

A small React example:

src/App.tsx
import { useState } from 'react'
export function App() {
const [name, setName] = useState('world')
return (
<>
<h3>Hello {name}!</h3>
<input value={name} onChange={(e) => setName(e.currentTarget.value)} />
</>
)
}

That's it — no Babel presets, no loader configuration, no resolve extensions. Vite handles TypeScript, JSX, and hot module replacement automatically.

Importing CSS

Vite supports importing CSS files directly from your TypeScript:

import { useState } from 'react'
import './App.css'
export function App() {
const [name, setName] = useState('world')
return (
<>
<h3>Hello {name}!</h3>
<input value={name} onChange={(e) => setName(e.currentTarget.value)} />
</>
)
}

For component-scoped styles, Vite supports CSS modules out of the box — any file ending in .module.css is treated as a CSS module:

.title {
color: steelblue;
}

Transforming CSS

Just like Babel once did transformations over JavaScript, PostCSS does for CSS. PostCSS alone doesn't actually do anything — it only provides a way to parse and transform CSS via plugins.

Vite has built-in support for PostCSS. Just add a config file and Vite applies it automatically:

npm i -D postcss-preset-env cssnano

postcss-preset-env lets you use future CSS features today by automatically adding vendor prefixes and polyfills based on your browserslist definition. cssnano minifies your CSS in production.

No loader configuration needed — Vite picks up postcss.config.js automatically.

Building for production

terminal
npx vite build

This generates optimized output in the dist/ folder. Vite automatically:

  • Hashes filenames for cache busting (e.g. assets/index-a1b2c3d4.js)
  • Extracts CSS into separate files
  • Code-splits dynamic imports into separate chunks
  • Minifies both JS and CSS

You can preview the production build locally with:

terminal
npx vite preview

Type checking

Vite transpiles TypeScript using Oxc (via Rolldown), which is extremely fast but does not perform type checking. For that, we'll use tsgo, the native Go port of the TypeScript compiler:

terminal
npm i -D @typescript/native-preview

tsgo is roughly 10x faster than tsc — the VSCode codebase (1.5 million lines) compiles in under 9 seconds instead of 89. It's a drop-in replacement for type checking:

tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"strict": true,
"noEmit": true,
"skipLibCheck": true,
"isolatedModules": true,
"resolveJsonModule": true,
"allowImportingTsExtensions": true
},
"include": ["src"]
}
terminal
npx tsgo -p .

This will report any type errors without emitting files — Vite handles the actual compilation.

Linting and formatting

The oxc project provides fast, Rust-based replacements for ESLint and Prettier: oxlint for linting and oxfmt for formatting.

terminal
npm i -D oxlint

oxlint works out of the box with zero configuration — it has sensible defaults and understands TypeScript and JSX natively:

terminal
npx oxlint src

It's orders of magnitude faster than ESLint and catches many of the same issues. You can configure it with an oxlintrc.json if you need to adjust rules:

oxlintrc.json
{
"rules": {
"no-unused-vars": "warn"
}
}

For formatting, oxfmt is a drop-in replacement for Prettier:

terminal
npx oxfmt src --write

Linting CSS

You can also bring the power of linting to your CSS using stylelint:

npm i -D stylelint stylelint-config-standard

Task running

We can save our different commands in package.json for easy re-use:

package.json
"scripts": {
"dev": "vite",
"build": "tsgo -p . && vite build",
"preview": "vite preview",
"typecheck": "tsgo -p .",
"lint": "oxlint src",
"fmt": "oxfmt src --write",
"lint:css": "stylelint 'src/**/*.css'"
}

During development you just need npm run dev. For CI or before deploying, npm run build will type-check first and then bundle.

Comments

  • Loading...