This post will walk through the process of converting an existing project to Vite. We’ll cover things like aliases, shimming webpack’s dotenv handling, and server proxying. In other words, we’re looking at how to move a project from its existing bundler to Vite. If you’re looking instead to start a fresh project, you’ll want to jump to their documentation.
Long story, short: the CLI will ask for your framework of choice—React, Preact, Svelte, Vue, Vanilla, or even lit-html—and whether you want TypeScript, then give you a fully functioning project.
Scaffold first! If you are interested in learning about integrating Vite into a legacy project, I’d still recommend scaffolding an empty project and poking around it a bit. At times, I’ll be pasting some clumps of code, but most of that comes straight from the default Vite template.
Our use case
What we’re looking at is based on my own experience migrating the webpack build of my booklist project (repo). There isn’t anything particularly special about this project, but it’s fairly big and old, and leaned hard on webpack. So, in that sense, it’s a good opportunity to see some of Vite’s more useful configuration options in action as we migrate to it.
What we won’t need
One of the most compelling reasons to reach for Vite is that it already does a lot right out of the box, incorporating many of the responsibilities from other frameworks so there are fewer dependencies and a more established baseline for configurations and conventions.
So, instead of starting by calling out what we need to get started, let’s go over all the common webpack things we don’t need because Vite gives them to us for free.
Static asset loading
We usually need to add something like this in webpack:
This takes any references to font files, images, SVG files, etc., and copies them over to your dist folder so they can be referenced from your new bundles. This comes standard in Vite.
I say “styles” as opposed to “css” intentionally here because, with webpack, you might have something like this:
test: /.s?css$/, use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"] , // later new MiniCssExtractPlugin( filename: "[name]-[contenthash].css" ),
…which allows the application to import CSS or SCSS files. You’ll grow tired of hearing me say this, but Vite supports this out of the box. Just be sure to install Sass itself into your project, and Vite will handle the rest.
Transpilation / TypeScript
…with a corresponding Babel configuration to specify the appropriate plugins which, for me, looked like this:
"presets": ["@babel/preset-typescript"], "plugins": [ "@babel/plugin-proposal-class-properties", "@babel/plugin-syntax-dynamic-import", "@babel/plugin-proposal-optional-chaining", "@babel/plugin-proposal-nullish-coalescing-operator" ]
While I could have probably stopped using those first two plugins years ago, it doesn’t really matter since, as I’m sure you’ve guessed, Vite does this all for us. It takes your code, removes any TypeScript and JSX, and produces code supported by modern browsers.
If you’d like to support older browsers (and I’m not saying you should), then there’s a plugin for that.
Surprisingly, webpack requires you to tell it to resolve imports from
node_modules, which we do with this:
resolve: modules: [path.resolve("./node_modules")]
As expected, Vite already does this.
One of the common things we do in webpack is distinguish between production and development environments by manually passing a
mode property, like this:
mode: isProd ? "production" : "development",
…which we normally surmise with something like this:
const isProd = process.env.NODE_ENV == "production";
And, of course, we set that environment variable via our build process.
Vite handles this a bit differently and gives us different commands to run for development builds versus those for production, which we’ll get into shortly.
At the risk of belaboring the point, I’ll quickly note that Vite also doesn’t require you to specify every file extension you’re using.
resolve: extensions: [".ts", ".tsx", ".js"],
Just set up the right kind of Vite project, and you’re good to go.
Rollup plugins are compatible!
This is such a key point I wanted to call it out in its own section. If you still wind up with some webpack plugins you need to replace in your Vite app when you finish this blog post, then try to find an equivalent Rollup plugin and use that. You read that correctly: Rollup plugins are already (or usually, at least) compatible with Vite. Some Rollup plugins, of course, do things that are incompatible with how Vite works—but in general, they should just work.
For more info, check out the docs.
Your first Vite project
Remember, we’re moving an existing legacy webpack project to Vite. If you’re building something new, it’s better to start a new Vite project and go from there. That said, the initial code I’m showing you is basically copied right from what Vite scaffolds from a fresh project anyway, so taking a moment to scaffold a new project might also a good idea for you to compare processes.
The HTML entry point
Here’s the HTML file (which Vite expects to be called
index.html) we’re starting with:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>The GOAT of web apps</title> </head> <body> <div id="home"></div> <script type="module" src="/reactStartup.tsx"></script> </body> </html>
Note that the
<script> tag points to
/reactStartup.tsx. Adjust that to your own entry as needed.
Let’s install a few things, like a React plugin:
npm i vite @vitejs/plugin-react @types/node
We also create the following
vite.config.ts right next to the
index.html file in the project directory.
import defineConfig from "vite"; import react from "@vitejs/plugin-react"; export default defineConfig( plugins: [react()] );
Lastly, let’s add a few new npm scripts:
"dev": "vite", "build": "vite build", "preview": "vite preview",
Now, let’s start Vite’s development server with
npm run dev. It’s incredibly fast, and incrementally builds whatever it needs to, based on what’s requested.
But, unfortunately, it fails. At least for right now.
We’ll get to how to set up aliases in a moment, but for now, let’s instead modify our
reactStartup file (or whatever your entry file is called) as follows:
import React from "react"; import render from "react-dom"; render( <div> <h1>Hi there</h1> </div>, document.getElementById("home") );
Now we can run it our
npm run dev command and browse to
Hot module reloading (HMR)
Now that the development server is running, try modifying your source code. The output should update almost immediately via Vite’s HMR. This is one of Vite’s nicest features. It makes the development experience so much nicer when changes seem to reflect immediately rather than having to wait, or even trigger them ourselves.
The rest of this post will go over all the things I had to do to get my own app to build and run with Vite. I hope some of them are relevant for you!
It’s not uncommon for webpack-based projects to have some config like this:
resolve: alias: jscolor: "util/jscolor.js" , modules: [path.resolve("./"), path.resolve("./node_modules")]
This sets up an alias to
jscolor at the provided path, and tells webpack to look both in the root folder (
./) and in
node_modules when resolving imports. This allows us to have imports like this:
import thing from "util/helpers/foo"
…anywhere in our component tree, assuming there’s a
util folder at the very top.
Vite doesn’t allow you to provide an entire folder for resolution like this, but it does allow you to specify aliases, which follow the same rules as the @rollup/plugin-alias:
import defineConfig from "vite"; import react from "@vitejs/plugin-react"; import path from "path"; export default defineConfig( resolve: alias: jscolor: path.resolve("./util/jscolor.js"), app: path.resolve("./app"), css: path.resolve("./css"), util: path.resolve("./util") , plugins: [react()] );
We’ve added a
resolve.alias section, including entries for everything we need to alias. Our
jscolor util is set to the relevant module, and we have aliases for our top-level directories. Now we can import from
util/ from any component, anywhere.
Note that these aliases only apply to the root of the import, e.g.
util/foo. If you have some other util folder deeper in your tree, and you reference it with this:
import thing from "./helpers/util";
…then the alias above will not mess that up. This distinction is not well documented, but you can see it in the Rollup alias plugin. Vite’s alias matches that same behavior.
Vite, of course, supports environment variables. It reads config values out of your
.env files in development, or
process.env, and injects them into your code. Unfortunately, things work a bit differently than what you might be used to. First, it does not replace
process.env.FOO but rather
import.meta.env.FOO. Not only that, but it only replaces variables prefixed with
VITE_ by default. So,
import.meta.env.VITE_FOO would actually be replaced, but not my original
FOO. This prefix can be configured, but not set to empty string.
For a legacy project, you could grep and replace all your environment variables to use
import.meta.env, then add a
VITE_ prefix, update your
.env files, and update the environment variables in whatever CI/CD system you use. Or you can configure the more classic behavior of replacing
process.env.ANYTHING with values from a
.env file in development, or the real
process.env value in production.
Here’s how. Vite’s
define feature is basically what we need. This registers global variables during development, and does raw text replacement for production. We need to set things up so that we manually read our
.env file in development mode, and the
process.env object in production mode, and then add the appropriate
Let’s build that all into a Vite plugin. First, run
npm i dotenv.
Now let’s look at the code for the plugin:
import dotenv from "dotenv"; const isProd = process.env.NODE_ENV === "production"; const envVarSource = isProd ? process.env : dotenv.config().parsed; export const dotEnvReplacement = () => const replacements = Object.entries(envVarSource).reduce((obj, [key, val]) => obj[`process.env.$key`] = `"$val"`; return obj; , ); return name: "dotenv-replacement", config(obj) ; ;
process.env.NODE_ENV for us, so all we need to do is check that to see which mode we’re in.
Now we get the actual environment variables. If we’re in production, we grab
process.env itself. If we’re in dev, we ask dotenv to grab our
.env file, parse it, and get back an object with all the values.
Our plugin is a function that returns a Vite plugin object. We inject our environment values into a new object that has
process.env. in front of the value, and then we return our actual plugin object. There is a number of hooks available to use. Here, though, we only need the
config hook, which allows us to modify the current config object. We add a
define entry if none exists, then add all our values.
But before moving forward, I want to note that the Vite’s environment variables limitations we are working around exist for a reason. The code above is how bundlers are frequently configured, but that still means any random value in
process.env is stuck into your source code if that key exists. There are potential security concerns there, so please keep that in mind.
But what if your web app is small enough that you have some services running right on your web server? For the project I’m converting, I have a GraphQL endpoint running on my web server. For development, I start my Express server, which previously knew how to serve the assets that webpack generated. I also start a webpack watch task to generate those assets.
But with Vite shipping its own dev server, we need to start that Express server (on a separate port than what Vite uses) and then proxy calls to
/graphql over to there:
server: proxy: "/graphql": "http://localhost:3001"
This tells Vite that any requests for
/graphql should be sent to
Note that we do not set the proxy to
http://localhost:3001/graphql in the config. Instead, we set it to
http://localhost:3001 and rely on Vite to add the
/graphql part (as well any any query arguments) to the path.
build: outDir: "./public", lib: entry: "./src/index.ts", formats: ["cjs"], fileName: "my-bundle.js"
Tell Vite where to put the generated bundle, what to call it, and what formats to build. Note that I’m using CommonJS here instead of ES modules since the ES modules do not minify (as of this writing) due to concerns that it could break tree-shaking.
You’d run this build with
vite build. To start a watch and have the library rebuild on change, you’d run
vite build --watch.
Adding Vite to Your Existing Web App originally published on CSS-Tricks. You should get the newsletter and become a supporter.