How to integrate Vite with WordPress development?

I will discuss how to integrate Vite step by step into the process of building and deploying plugins or dedicated themes based on WordPress.

GitHub

You're wasting time not using this 🫵

We've all been there, putting everything into a single file. It's just a universal developer experience, as well as refreshing the page manually after each change in the code, isn't it? Hovewer, what if I told you there's a better way? The way that will help you gaining more with less effort while building assets to production.

What if you could improve code readability by using multiple files and not having to worry about optimizing and loading them? What if you could build the look of your app and see the results in the browser faster than the blink of an eye, avoiding wasting time on tedious page refreshes? What if you didn't have to worry about old browser support?

If any of these aspects made your heart beat faster, I insist you to stay with me and check how Vite can help you. I will discuss how to integrate Vite step by step into the process of building and deploying plugins or dedicated themes based on WordPress.



What is Vite?

Vite is widely known in the frontend world, particularly with frameworks like Vue. It provides an extremely fast development environment and bundles code for production. Developers love it because it speeds up and makes development process more efficient.

It makes using modern browser features easier and organizes the pieces of your assets in the way help your website load faster without additional effort. It's like having assistant that will pack your suitcase before flight faster and more efficient, while you can focus on more friendly things, like taking a shower. It also has a cool feature called Hot Module Reload (HMR) which allows seeing the results of your work faster.


Why to use Vite with WordPress?

Since Vite has been created to build frontend apps it isn't limited to them. It's flexible enough to be used in the backend too, making it a handy tool for all sorts of web apps. Laravel for example, uses Vite as default bundler so we can do the same in WordPress. But actually why would we want to? Let's analyze the most important aspects.

Be aware, that this part is not limited just to Vue. It's more like general description why it is worth to use bundling tools in WordPress development.

#1 Bundling helps improving code readability

Let's get back to your trip. To make it easier to pack or look for things after arrival, you group them and spread them out on the bed. Sure, it takes up more space, but it's just easier for you. Finally, when you have everything, you call Vite and say "pack it for me". The Vite will do it faster and better. You'll just have to take luggage to airport. Do you see analogy? It's just like building the code and packing assets to deploy them on prod.

Breaking down the source code into smaller self-efficient modules makes the codebase more organized, user-friendly, and easier to maintain. Smaller CSS/JS files with specific functionality are not only easier to read but also better for the teamwork. Vite allows merging (bundling) them into one or more files so we don't need to enqueue them one by one manually. We can enqueue just a few production-ready assets instead.

#2 Bundling improves application performance

When we go on a trip with my wife, she usually has to take more than she can fit in the suitcase. What husband does not know this? I usually dream about a vacuum cleaner and vacuum bags in such situations to reduce the luggages' volume and pay less, or at least please my wife. And that's what the Vite does in the bundling process.

When Vite bundles multiple smaller files into one, it additionally performs step called minification. It typically includes removing comments, extra spaces, line breaks, and shortening variable and function names resulting in smaller file sizes. Futhermore, it compresses output files what speeds up the loading times and reduces bandwidth. So it just produces the code that does the same things as earlier, but with smaller size.

#3 Bundling improves old-browsers support

From time to time I go on vacation with grandparents. As we all know, they mostly don't have a sence of humors as we have and in some cases we need to explain memes that we laugh on to them. Vite can make this for us!

JS/CSS continually introduce new features, but not all are supported by older browsers. Vite allows configuring tools called polyfills which reduce compatibility problems while creating modern code. They translates modern features to the code understandable by old browsers during the build process. It results in less work to do with better results.

#4 Bundling improves development experience

Vite supports feature called Hot Module Replacement (HMR). Unlike standard refreshing the page to see the results, HMR tries to update only the parts of code that have changed, preserving the application's state and avoiding refresh resulting in instantly seeing the results in the browser as we make them in the code.

For example, making the changes in CSS files is immediately reflected in the browser window after file save without refreshing the page. It works the same for JS files, but... The code must be . Otherwise, changes to JS files will trigger a automatic refresh, a process known as Hot Reload.


How to organize assets in the codebase?

Let's start by organizing assets and setting up the directory structure for them. If you recall from my previous post I already created the resources directory for storing our Blade views. Now, I'll take a step further and create a few additional directories.

To keep things super simple and easy to access, I organize these assets by type. This way, finding what I need is easy whenever I require it. I've also created a few files and filled them with some to have sources that can be used for building process.

├── resources
│   ├── fonts
│   │   ├── **/*.woff2
│   ├── images
│   │   ├── **/*.*
│   ├── scripts
│   │   ├── **/*.js
│   ├── styles
│   │   ├── **/*.scss

To minimize code duplication, I set up a few constants for easier access to URIs and paths. While I could have used functions like get_template_directory offered by WordPress, I prefer to keep the codebase flexible, not confined solely to themes. In upcoming releases, you'll see its ability to function effectively as a plugin as well.

define('FM_VERSION', '0.1.1');
define('FM_PATH', dirname(__FILE__));
define('FM_URI', home_url(str_replace(ABSPATH, '', FM_PATH)));
define('FM_HMR_HOST', 'http://localhost:5173');
define('FM_ASSETS_PATH', FM_PATH . '/dist');
define('FM_ASSETS_URI', FM_URI . '/dist');
define('FM_RESOURCES_PATH', FM_PATH . '/resources');
define('FM_RESOURCES_URI', FM_URI . '/resources');

How to build assets with Vite in WordPress?

Now, it's time to set up Vite for my project to build production release. I use Yarn as a package manager, but feel free to use what you like. Vite is flexible enough to handle various options. Oh, and because I work with Sass for styling, I install that too.

yarn add vite sass --dev

Next up is configuration. When running vite from the command line, it automatically looks for a file named vite.config.js inside project root. This file allows defining how Vite operates within the project so let's bundling process.

import path from 'path';
import { defineConfig } from 'vite';

const ROOT = path.resolve('../../../')
const BASE = __dirname.replace(ROOT, '');

export default defineConfig({
  base: process.env.NODE_ENV === 'production' ? `${BASE}/dist/` : BASE,
  build: {
    manifest: true,
    assetsDir: '.',
    outDir: `dist`,
    emptyOutDir: true,
    sourcemap: true,
    rollupOptions: {
      input: [
        'resources/scripts/scripts.js',
        'resources/styles/styles.scss',
      ],
      output: {
        entryFileNames: '[hash].js',
        assetFileNames: '[hash].[ext]',
      },
    },
  },
  plugins: [
    {
      name: 'php',
      handleHotUpdate({ file, server }) {
        if (file.endsWith('.php')) {
          server.ws.send({ type: 'full-reload' });
        }
      },
    },
  ],
});

To generate production build I need to add a new tasks to package.json file and run yarn build comand in the project root. The assets with manifest.json mapping file in the dist directory.

{
  "scripts": {
    "build": "vite build",
    "dev": "vite"
  }
}

The production bundle assumes support for modern JavaScript. By default, Vite targets browsers which support the native ES Modules, native ESM dynamic import, and import.meta, that's why we need to load scripts with type="module" attribute.

Chrome >=87
Firefox >=78
Safari >=14
Edge >=88

We can specify custom targets via build.target config, but keep in mind that Vite only handles syntax transforms and does not cover polyfills. If you want to support older browsers than they, you can follow the steps described in Vite documentation or


How to enqueue assets generated by Vite?

The build process is running smoothly, and the output files are generated in the dist directory. Now, it's time to enqueue them in WordPress. I'll handle this within the newly created FM\Assets module designed to manage assets in my project.

├── app
│   ├── Assets
│   │   ├── Assets.php
│   │   ├── Resolver.php

The output file names are hashed so I need to search for the final URL in the file. To split responsibilities and improve core readability by moving backend details to separate file I decide to use what in simple words allows to reuse code in classes.

I could skip trait usage, initialize new Resolver instance in the class property and use it for enqueueing assets - what seems to be more correct approach - but I wanted to show how the traits can be used. I build Resolver trait that works .

namespace FM\Assets;

trait Resolver
{
    private array $manifest = [];

    /**
     * @action wp_enqueue_scripts 1
     */
    public function load(): void
    {
        $path = fm()->config()->get('manifest.path');

        if (empty($path) || ! file_exists($path)) {
            wp_die(__('Run <code>npm run build</code> in your application root!', 'fm'));
        }

        $this->manifest = json_decode(file_get_contents($path), true);
    }

    /**
     * @filter script_loader_tag 1 3
     */
    public function module(string $tag, string $handle, string $url): string
    {
        if ((false !== strpos($url, FM_HMR_HOST)) || (false !== strpos($url, FM_ASSETS_URI))) {
            $tag = str_replace('<script ', '<script type="module" ', $tag);
        }

        return $tag;
    }

    private function resolve(string $path): string
    {
        $url = '';

        if (! empty($this->manifest["resources/{$path}"])) {
            $url = FM_ASSETS_URI . "/{$this->manifest["resources/{$path}"]['file']}";
        }

        return apply_filters('fm/assets/resolver/url', $url, $path);
    }
}

Now, I set up a main module controller, integrate the Resolver trait there and enqueue assets using WordPress functions - wp_enqueue_style or wp_enqueue_script.

namespace FM\Assets;

use FM\Assets\Resolver;

class Assets
{
    use Resolver;

    /**
     * @action wp_enqueue_scripts
     */
    public function front(): void
    {
        wp_enqueue_style('theme', $this->resolve('styles/styles.scss'), [], fm()->config()->get('version'));
        wp_enqueue_script('theme', $this->resolve('scripts/scripts.js'), [], fm()->config()->get('version'));
    }
}

Here, lies a great example how I like to simplicity the code for other team members with keeping the code clean. Assets are often managed by frontent team and it's easier for them to do this in simple class like above rather than in something . I don't bother them with more complicated backend mechanisms which I hide in traits.


How to integrate Vite HMR in WordPress?

Running vite command in the project root fires up dev server, typically available at http://localhost:5173 which needs to be integrated with WordPress. This server takes care of serving assets and handling Hot Module Replacement (HMR).

I start from extending app config with HMR setup. I set server host, client url, base path, and the value that specifies if the dev server is running. I set this by checking if development mode is enabled and if the vite server is available with wp_remote_get.

$this->config = [
    'hmr' => [
        'host' => FM_HMR_HOST,
        'client' => FM_HMR_HOST . '/@vite/client',
        'base' => str_replace(home_url(), FM_HMR_HOST, FM_RESOURCES_URI),
        'active' => wp_get_environment_type() === 'development' && ! is_wp_error(wp_remote_get(FM_HMR_HOST)),
    ],
];

Then, I need to include vite client and inform WordPress that it should use the assets provided by the vite server rather than the dist directory. Vite, being an external tool, should have minimal impact on my codebase, so I create a dedicated module that handles and only when dev server is running.

namespace FM\Integrations;

class Vite
{
    /**
     * @action wp_head 1
     */
    public function client(): void
    {
        echo '<script type="module" src="' . fm()->config()->get('hmr.client') . '"></script>';
    }

    /**
     * @filter fm/assets/resolver/url 1 2
     */
    public function url(string $url, string $path): string
    {
        return fm()->config()->get('hmr.base') . "/{$path}";
    }
}

This design choice adheres to the Open-Closed Principle, allowing me to easily switch to a different tool without modifying tons of core files. When I resign from using Vite, I can remove integration class, it's initialization without affecting Assets core module at all. I just unplug this small brick from the building without modifying general structure.