Owen Conti

Replacing Laravel Mix with Vite

Posted on under Laravel by Owen Conti.

Stop, please read this before continuing!

As of Laravel 9.19, Laravel uses Vite to compile assets with official support. If you're using a Laravel version above 9.19, please do not use this guide and instead follow the official Laravel documentation.

This article was written before Laravel had official support for Vite.

In this guide, we'll replace Laravel Mix with Vite in a Laravel Jetstream (Inertia/Vue) application.

Vite is a build tool created by Evan You (creator of Vue) which utilizes the availability of native ES modules in the browser. Read more about Vite on the Why Vite page.

NOTE: This is my first time using Vite. I do not have a full understanding of Vite at the time of writing this post. If you see anything incorrect with this setup, please let me know on Twitter, @owenconti.

TLDR;

If you want to get up and running right away, consider using the Laravel Vite package created by Enzo Innocenzi which is an opinionated setup that handles everything for you.

TLDR; extended (Vue 3)

Vite recommends not omitting file extensions for custom import types, ie: .vue files. https://vitejs.dev/config/#resolve-extensions

This means you should ensure all of your imports use the .vue extension throughout your codebase.

Install npm dependencies:

1npm install --save-dev vite @vitejs/plugin-vue dotenv @vue/compiler-sfc

Uninstall Laravel Mix dependency and remove the config file:

1npm uninstall laravel-mix
2rm webpack.mix.js
3rm webpack.config.js

Setup PostCSS:

1// postcss.config.js
2 
3module.exports = {
4 plugins: [
5 require('postcss-import'),
6 require('tailwindcss')
7 ]
8}

Create vite.config.js file:

1import vue from '@vitejs/plugin-vue';
2const { resolve } = require('path');
3const Dotenv = require('dotenv');
4 
5Dotenv.config();
6 
7const ASSET_URL = process.env.ASSET_URL || '';
8 
9export default {
10 plugins: [
11 vue(),
12 ],
13 
14 root: 'resources',
15 base: `${ASSET_URL}/dist/`,
16 
17 build: {
18 outDir: resolve(__dirname, 'public/dist'),
19 emptyOutDir: true,
20 manifest: true,
21 target: 'es2018',
22 rollupOptions: {
23 input: '/js/app.js'
24 }
25 },
26 
27 server: {
28 strictPort: true,
29 port: 3000
30 },
31 
32 resolve: {
33 alias: {
34 '@': '/js',
35 }
36 },
37 
38 optimizeDeps: {
39 include: [
40 'vue',
41 '@inertiajs/inertia',
42 '@inertiajs/inertia-vue3',
43 '@inertiajs/progress',
44 'axios'
45 ]
46 }
47}

Ensure environment variables are set:

1// .env
2 
3// Running locally
4APP_ENV=local
5ASSET_URL=http://localhost:3000
6 
7// Running production build
8APP_ENV=production
9ASSET_URL=https://your-asset-domain.com

Install Laravel Vite Manifest PHP package to include Vite's output files from the generated manifest:

1composer require ohseesoftware/laravel-vite-manifest

Add the Blade directive from the package which includes the generated assets:

1// app.blade.php
2 
3<head>
4 // ... rest of head contents here
5 @vite
6</head>

Add npm scripts to run Vite:

1// package.json
2 
3"scripts": {
4 "start": "vite",
5 "production": "vite build"
6},

Import your .css file inside your entry .js file:

1// app.js
2 
3import '../css/app.css';

Make a few minor changes for Inertia:

1// Add the polyfill for dynamic imports to the top of your entry .js file
2 
3import 'vite/dynamic-import-polyfill';
4 
5// Change how dynamic pages are loaded
6 
7const pages = import.meta.glob('./Pages/**/*.vue');
8 
9// Update the `resolveComponent` logic
10 
11resolveComponent: name => {
12 const importPage = pages[`./Pages/${name}.vue`];
13 
14 if (!importPage) {
15 throw new Error(`Unknown page ${name}. Is it located under Pages with a .vue extension?`);
16 }
17 
18 return importPage().then(module => module.default);
19}

That's the quick version. If you want to understand more about each step, keep reading.

Walkthrough

Here's what we're going to setup:

  • Replace a default Laravel Mix setup

  • Compile JS (Vue)

  • Compile CSS (Tailwind)

If your existing Laravel Mix is more complicated than that, your mileage may vary with this guide.

We'll setup both a Vue 2 version and a Vue 3 version.

Setting up the vite.config.js file

To start off, we need to create a vite.config.js file in the root of the repo.

Vue 2:

1import { createVuePlugin as Vue2Plugin } from 'vite-plugin-vue2';
2const { resolve } = require('path');
3const Dotenv = require('dotenv');
4 
5Dotenv.config();
6 
7const ASSET_URL = process.env.ASSET_URL || '';
8 
9export default {
10 plugins: [
11 Vue2Plugin(),
12 ],
13 
14 root: 'resources',
15 base: `${ASSET_URL}/dist/`,
16 
17 build: {
18 outDir: resolve(__dirname, 'public/dist'),
19 emptyOutDir: true,
20 manifest: true,
21 target: 'es2018',
22 rollupOptions: {
23 input: '/js/app.js'
24 }
25 },
26 
27 server: {
28 strictPort: true,
29 port: 3000
30 },
31 
32 resolve: {
33 alias: {
34 '@': '/js',
35 }
36 },
37 
38 optimizeDeps: {
39 include: [
40 'vue',
41 'portal-vue',
42 '@inertiajs/inertia',
43 '@inertiajs/inertia-vue',
44 '@inertiajs/progress',
45 'axios'
46 ]
47 }
48}

Vue 3:

1import vue from '@vitejs/plugin-vue';
2const { resolve } = require('path');
3const Dotenv = require('dotenv');
4 
5Dotenv.config();
6 
7const ASSET_URL = process.env.ASSET_URL || '';
8 
9export default {
10 plugins: [
11 vue(),
12 ],
13 
14 root: 'resources',
15 base: `${ASSET_URL}/dist/`,
16 
17 build: {
18 outDir: resolve(__dirname, 'public/dist'),
19 emptyOutDir: true,
20 manifest: true,
21 target: 'es2018',
22 rollupOptions: {
23 input: '/js/app.js'
24 }
25 },
26 
27 server: {
28 strictPort: true,
29 port: 3000
30 },
31 
32 resolve: {
33 alias: {
34 '@': '/js',
35 }
36 },
37 
38 optimizeDeps: {
39 include: [
40 'vue',
41 '@inertiajs/inertia',
42 '@inertiajs/inertia-vue3',
43 '@inertiajs/progress',
44 'axios'
45 ]
46 }
47}

Let's run through each section.

Plugins

The plugins section tells Vite how to handle .vue files.

1// Vue 2
2import { createVuePlugin as Vue2Plugin } from 'vite-plugin-vue2';
3 
4export default {
5 plugins: [
6 Vue2Plugin(),
7 ]
8}
9 
10// Vue 3
11import vue from '@vitejs/plugin-vue';
12 
13export default {
14 plugins: [
15 vue(),
16 ]
17}

Root

The root option tells Vite what directory is the root directory of our application. Assets will be output relative from this directory. For example, resources/js/app.js will be output as js/app.js.

1// ...
2 
3root: 'resources',

Base

The base option tells Vite where the assets will be served from once deployed. This is equivalent to the publicPath option in Webpack. We pull the ASSET_URL from the environment file so that our build uses the correct path when deployed to a CDN.

Note: Make sure the ASSET_URLis set correctly in your .env file when building. If you're deploying with Vapor, Vapor will set the ASSET_URL for you.

1// ...
2 
3base: `${ASSET_URL}/dist/`,

Build

The build section tells Vite how the application should be built.

  • outDir - The output directory that the application should be built to.

  • emptyOutDir - We set this to true to suppress a warning from Vite that says we are emptying the outDir when it exists outside project root (resources).

  • manifest - Tells Vite to publish a manifest file, which we'll use in production builds to find the correct file hash names.

  • target - Tells Vite which browsers should be supported, you can read more on Vite's website.

  • rollupOptions - These are specific options you can provide to Rollup (which Vite uses to bundle the application). In our case, we need to provide Rollup with our main entry file.

1const { resolve } = require('path');
2 
3// ...
4 
5build: {
6 outDir: resolve(__dirname, 'public/dist'),
7 emptyOutDir: true,
8 manifest: true,
9 target: 'es2018',
10 rollupOptions: {
11 input: '/js/app.js'
12 }
13}

Server

The server section instructs Vite on how to start the development server.

  • strictPort - Forces Vite to start on the port we specified. Vite will exit if the port is in use as opposed to incrementing the port number which is default behaviour.

  • port - Which port the Vite development server should run on.

1server: {
2 strictPort: true,
3 port: 3000
4},

Resolve

The resolve section is optional. In my case, I am using it to alias @ to /js.

1resolve: {
2 alias: {
3 '@': '/js',
4 }
5},

Optimize Dependencies

We need to tell Vite to pre-bundle the dependencies that do not ship a ESM version. The array you pass here will vary based on the dependencies in your project.

Note: portal-vue is not necessary in Vue 3 projects.

1optimizeDeps: {
2 include: [
3 'vue',
4 'portal-vue', // Vue 2
5 '@inertiajs/inertia',
6 '@inertiajs/inertia-vue', // Vue 2
7 '@inertiajs/inertia-vue3', // Vue 3
8 '@inertiajs/progress',
9 'axios'
10 ]
11}

Dependencies to install

You'll need to make sure you install the following JS dependencies:

1// Vue 2
2npm install --save-dev vite vite-plugin-vue2 dotenv @vue/compiler-sfc
3 
4// Vue 3
5npm install --save-dev vite @vitejs/plugin-vue dotenv @vue/compiler-sfc

Setup PostCSS

In order to compile Tailwind, we need to move our PostCSS configuration from webpack.mix.js into a dedicated postcss.config.js file, which resides at the root of your repo:

1// postcss.config.js
2 
3module.exports = {
4 plugins: [
5 require('postcss-import'),
6 require('tailwindcss')
7 ]
8}

Update your Inertia JS setup

Here's my full app.js that configures Inertia:

1import 'vite/dynamic-import-polyfill';
2 
3import { createApp, h } from 'vue';
4import { App as InertiaApp, plugin as InertiaPlugin } from '@inertiajs/inertia-vue3';
5import { InertiaProgress } from '@inertiajs/progress';
6 
7import axios from 'axios';
8axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
9 
10import '../css/app.css';
11 
12InertiaProgress.init();
13 
14const app = document.getElementById('app');
15 
16const pages = import.meta.glob('./Pages/**/*.vue');
17 
18createApp({
19 render: () =>
20 h(InertiaApp, {
21 initialPage: JSON.parse(app.dataset.page),
22 resolveComponent: name => {
23 const importPage = pages[`./Pages/${name}.vue`];
24 if (!importPage) {
25 throw new Error(`Unknown page ${name}. Is it located under Pages with a .vue extension?`);
26 }
27 return importPage().then(module => module.default)
28 }
29 }),
30})
31 .mixin({ methods: { route } })
32 .use(InertiaPlugin)
33 .mount(app);

Environment variables

The asset path is controlled via the standard Laravel ASSET_URL environment variable. However, we don't provide a default, so it must be set in order for everything to work.

We need to change a couple of environment variables based on if we're running the local development server vs if we're running a production build:

1// Running locally
2APP_ENV=local
3ASSET_URL=http://localhost:3000
4 
5// Running production build
6APP_ENV=production
7ASSET_URL=https://your-asset-domain.com

Install the Laravel Vite Manifest package

I wrote a very simple Laravel package to pull the contents of the Vite manifest and include them in your Blade view. The main logic for the package is sourced from https://github.com/andrefelipe/vite-php-setup.

The package uses the APP_ENV and ASSET_URL environment variables to decide how to load the assets.

1composer require ohseesoftware/laravel-vite-manifest

Add the Blade directive to include Vite's compiled assets:

1// app.blade.php
2 
3<head>
4 // ... rest of head contents here
5 @vite
6</head>

Remove Laravel Mix

Don't forget to remove Laravel Mix and its configuration file:

1npm uninstall laravel-mix

Thanks for reading this article!

Hopefully you found this article useful! If you did, share it on Twitter!

Found an issue with the article? Submit your edits against the repository.