Skip to content

Migrate from Webpack/Vue CLI to Vite

Migrating to Vue 3 is a great opportunity to upgrade your toolchain to Vite too! You’ll boost your development speed, streamline configurations, and simplify migrating to TypeScript, as Vite supports it out of the box.

This guide will take you step-by-step through refactoring your Vue application from Webpack/Vue CLI to Vite.

Initial Setup: Creating a New Vite Project

Let’s first set up a fresh Vite project as a reference.

For Vue, start with the official create-vue scaffolder. It creates a Vite-powered project and can include TypeScript, tests, Router, and Pinia:

Terminal window
npm create vue@latest
cd my-project

Current scaffolding tools require a current Node.js version. Check the Vue quick start before running the command; the current create-vue requirement is Node.js ^22.18.0 || >=24.12.0. Vite 8 itself supports Node.js 20.19+ or 22.12+. Explore the new project structure before proceeding.

To see Vite in action, run:

Terminal window
npm install
npm run dev

Migrating a Vue Project from Vue CLI

If your project is already on Vue 3, use the fresh project as a reference—but do not blindly copy src and configuration. First inventory Vue CLI plugins, aliases, environment access, HTML interpolation, require.context calls, test setup, and package scripts, then map each feature to its Vite equivalent.

// Original Vue CLI Structure
my-old-vue2-app/
├── src/
├── public/
├── vue.config.js
└── package.json
// New Vite Structure
my-project/
├── src/
├── public/
├── vite.config.js
└── package.json

For Vue 3, install Vite and the Vue plugin in the existing project if you are not using a fresh scaffold:

Terminal window
npm install -D vite @vitejs/plugin-vue

Vue 2.7 can use @vitejs/plugin-vue2, but the latest plugin supports Vite 3–7 rather than Vite 8, and Vue 2 is end of life. Pin compatible versions deliberately and treat this as a temporary migration step, not a current long-term stack.

Migrating a Project from Webpack

Start by creating an index.html and a vite.config.js.

Your index.html should contain at least this:

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My Title</title>
</head>
<body>
<div id="app"></div>
<!-- Update the path to your needs -->
<script type="module" src="/src/main.ts"></script>
</body>
</html>

In Vite, index.html acts as the entry point. If your project has multiple entry points, the next section will help you handle that.

We will now translate Webpack options one by one into the corresponding Vite options.

Entry Point, Mode, and Output

In Webpack, you’d define entry points, mode, and output like this:

webpack.config.js
const path = require("path");
module.exports = {
entry: "./src/index.js",
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "dist"),
},
mode: "development", // or 'production'
};

Vite automatically handles entry points and mode settings. By default, it uses index.html and outputs builds to dist. The mode is set to development for the dev server and to production for the build.

Multi-page applications and libraries use different configurations. For a multi-page application, point Rolldown at multiple HTML entry files:

vite.config.js
import { resolve } from "path";
import { defineConfig } from "vite";
export default defineConfig({
build: {
rolldownOptions: {
input: {
main: resolve(import.meta.dirname, "index.html"),
admin: resolve(import.meta.dirname, "admin/index.html"),
},
},
},
});

Library authors should instead use build.lib and externalize dependencies such as Vue according to the library’s packaging requirements.

If you need to change your Vite config depending on the mode, you can pass a function to defineConfig:

vite.config.js
import { defineConfig } from "vite";
export default defineConfig(({ mode }) => {
return {
// do something with mode
};
});

You can find more details in the official Vite build documentation.

Webpack Loaders

Webpack uses loaders to handle file transformations, while current Vite releases provide built-in handling for common web assets and use Oxc/Rolldown internally. Verify each loader’s behavior before removing it.

Here are common loaders that may be removable after you verify that Vite or an installed plugin covers the same behavior:

  • Babel Loader: Remove it only if you do not rely on custom Babel plugins, syntax transforms, or polyfill injection. Map every non-standard transformation to native Vite support or a compatible plugin first.
  • File Loader / URL Loader: Vite automatically handles static assets (e.g., images, SVGs, JSON). More details in the Vite documentation.
  • CSS/Sass/SCSS Loader / Style Loader: Vite offers native support for CSS and preprocessors like Sass/SCSS. Just make sure you have the necessary pre-processor installed. Importing CSS files will automatically add them to the DOM.
  • JSON Loader: JSON imports work out-of-the-box with Vite.

For other specific loaders, you might need a Vite plugin. While I can’t cover all loader types, note that Vite generally handles most use cases Webpack does, with few exceptions.

Static Assets with the public Directory

As previously mentioned, Vite automatically manages asset loading. However, if you need to serve assets directly without importing them, place them in the public directory. Files in this directory are accessible from the root path.

index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<!-- favicon stored as a static asset in the public directory -->
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My Title</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

Dev Server

Both Webpack and Vite offer development servers, but Vite’s is significantly faster, thanks to native ES module support.

webpack.config.js
module.exports = {
devServer: {
static: {
directory: "./dist",
},
hot: true,
},
};
vite.config.js
import { defineConfig } from "vite";
export default defineConfig({
server: {
open: true, // automatically opens the browser
hmr: true, // Hot Module Replacement
},
});

Aliases

Webpack and Vite have similar alias configuration, but Vite expects absolute filesystem replacement paths:

webpack.config.js
const path = require("path");
module.exports = {
resolve: {
alias: {
"@": path.resolve(__dirname, "src"),
},
},
};
vite.config.js
import { resolve } from "path";
export default {
resolve: {
alias: {
"@": resolve(import.meta.dirname, "src"),
},
},
};

Environment Variables

In Webpack, you need to explicitly load environment files (e.g., using dotenv) and pass the variables to your code via the DefinePlugin:

webpack.config.js
const webpack = require("webpack");
require("dotenv").config({ path: "./.env" });
module.exports = {
plugins: [
new webpack.DefinePlugin({
"process.env.PUBLIC_API_ORIGIN": JSON.stringify(
process.env.PUBLIC_API_ORIGIN,
),
}),
],
};

Vite loads environment variables prefixed with VITE_ and exposes them through import.meta.env. Never expose the entire build environment. If you intentionally need a differently named public value, load the environment for the active mode and define that one key explicitly:

vite.config.js
import { defineConfig, loadEnv } from "vite";
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), "");
return {
define: {
"import.meta.env.PUBLIC_API_ORIGIN": JSON.stringify(
env.PUBLIC_API_ORIGIN,
),
},
};
});

See the corresponding official docs for more details.

Custom Code Splitting (chunks)

While Vite automatically splits your code based on dynamic imports, Webpack allows you to manually control code splitting using special comments:

const UserDetails = () =>
import(/* webpackChunkName: "group-user" */ "./UserDetails");
const UserDashboard = () =>
import(/* webpackChunkName: "group-user" */ "./UserDashboard");
const UserProfileEdit = () =>
import(/* webpackChunkName: "group-user" */ "./UserProfileEdit");

Vite preserves code splitting created by dynamic imports. Do not copy webpack chunk comments or old build.rollupOptions.output.manualChunks examples into current Vite 8: Vite 8 uses Rolldown, and object-form manualChunks has been removed. First measure the default output. If manual grouping is still needed, use the current build.rolldownOptions.output.codeSplitting.groups API and match resolved module IDs carefully.

For example, this keeps Vue ecosystem dependencies in a stable vendor chunk and groups the three user-area modules from the example above into group-user:

vite.config.js
import { defineConfig } from "vite";
export default defineConfig({
build: {
rolldownOptions: {
output: {
codeSplitting: {
groups: [
{
name: "vue-vendor",
test: /node_modules[\\/](?:vue|vue-router|pinia)[\\/]/,
priority: 20,
},
{
name: "group-user",
test: /[\\/]src[\\/](?:UserDetails|UserDashboard|UserProfileEdit)\.(?:js|ts|vue)(?:\?|$)/,
priority: 10,
},
],
},
},
},
},
});

The test expressions run against resolved module IDs, and higher-priority groups win when more than one group matches. Keep the patterns narrow, run a production build, and inspect the resulting chunk graph: manual grouping can alter side-effect execution order and adds a small runtime.js chunk.

Framework Integration

To integrate your preferred framework with Vite, add the corresponding plugin to your project. Here’s how you can set it up in your vite.config.js:

vite.config.js
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
export default defineConfig({
plugins: [vue()],
});

Other Webpack Plugins

Webpack has a vast ecosystem of plugins, so make a feature-by-feature inventory rather than assuming direct compatibility. Prefer documented Vite plugins and verify that each plugin supports your installed Vite major version. Vite 8 uses Rolldown rather than Rollup for bundling.

Need Assistance?

If you need help with your migration, feel free to contact me via email or visit the contact page. I offer professional assistance and guidance for migrations at fair rates.