| Programming | 27th July 2024

Vite can be a great alternative to Webpack that sometimes can be difficult to setup and it needs a longer time to compile large amounts of code.

In a previous article you can see that even Vite can take a lot of time and frustration to set it up for specific cases, as it happen to me, even if is presented as being a much faster solution to start with than the well known Webpack.

The goal of making Vite to work in a WordPress theme

To have Vite working on a WordPress theme, for me, it is a must to have it working with HRM (Hot Module Replacement).

After many hours of investigation and research on the mighty Google I found things that sent me in the right direction.

You can see in the details bellow, the setup I came up with.

A manifest.json file content is generated for production only (when running npm run build) – this method is using the generated final assets files when for development mode no physical files are generated which is making the developing process much faster and the browser is loading ES modules.

Step by step here is the configuration of Vite in WordPress

I installed cross-env to can set an environment variable in node, that will tell me when I compile the code for development and when for production. I wanted to be able to generate the production files from my local environment.

Here is the packages.json file with the necessary packages…quite light

File location wp-theme/vite/packages.json

{
  "name": "vite-wp",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "cross-env NODE_ENV=development vite",
    "start": "cross-env NODE_ENV=development npm run dev",
    "build": "cross-env NODE_ENV=production vite build",
    "preview": "cross-env NODE_ENV=production vite preview"
  },
  "devDependencies": {
    "cross-env": "^7.0.3",
    "sass": "^1.64.1",
    "typescript": "^5.0.2",
    "vite": "^4.4.5"
  }
}

The next code is showing vite.config.ts with all the configuration needed

File location wp-theme/vite/vite.config.ts

The vite/dist directory is emptied every time the build is restarted

As for the default vite build all the entry files are first added to a wp-theme/vite/dist/tmp directory.

If the development mode is running the entries files are used directly through @vite but if production mode is running, the files are moved to wp-theme/vite/dist/js, respectively wp-theme/vite/dist/style directories

import { defineConfig } from 'vite';
import { rm, mkdir, writeFile } from 'node:fs/promises'
import { resolve } from 'path'
import { CopyFilePlugin } from './plugins';

const getEntries = (entries): any => { const out = {}; entries.forEach(entry => { out[entry.name] = entry.source; }); return out; };

const entries = [
  { name: 'index', source: './ts/index.ts', type: 'js' },
  { name: 'style', source: './scss/style.ts', type: 'css' },
];

export default defineConfig(({ command }) => {
  return {
    plugins: [
      ...entries.map(entry => CopyFilePlugin({
        sourceFileName: entry.type === 'css' ? `${entry.name}.css` : entry.name,
        absolutePathToDestination: entry.type === 'css' ? resolve(__dirname, './dist/style/') : resolve(__dirname, './dist/js/'),
      })),
      {
        name: "Cleaning theme directory",
        async buildStart() {
          // for development
          await rm(resolve(__dirname, './dist/js'), { recursive: true, force: true });
          await rm(resolve(__dirname, './dist/style'), { recursive: true, force: true });
          await mkdir(resolve(__dirname, './dist/js'), { recursive: true });
          await mkdir(resolve(__dirname, './dist/style'), { recursive: true });
          
          if(process.env.NODE_ENV === 'development'){
            console.log('*****development build*****');
            // for development -> empty the manifest file to check on php for an empty object
            const out = {};
            const jsonData = JSON.stringify(out);
            await rm(resolve(__dirname, '../manifest.json'), { force: true });
            await writeFile(resolve(__dirname, '../manifest.json'), jsonData);
          }
        }
      },
      {
        name: "Create Manifest",
        writeBundle: async (data: any, output) => {
          console.log('*****production build*****');
          // for production
          const out = {};
          entries.map(entry => {
            const isStyle = entry.type === 'css';
            const sourceName = isStyle ? `${entry.name}.css` : entry.name;
            const destination = isStyle ? '/vite/dist/style' : '/vite/dist/js';
            const item = Object.values(output).find(({ name }) => name === sourceName);
            if (!item) return;
            out[sourceName] = item.fileName.replace('assets', destination);
          });

          const jsonData = JSON.stringify(out);
          await rm(resolve(__dirname, '../manifest.json'), { force: true });
          await writeFile(resolve(__dirname, '../manifest.json'), jsonData);
        }
      },
    ],
    build: {
      target: 'modules',
      outDir: 'dist/tmp', // initial build place
      rollupOptions: { input: getEntries(entries) },
      emptyOutDir: true,
    },
    server: {
      port: 1337,
      host: '0.0.0.0',
    },
  }
});

The code under CopyFilePlugin needed to copy the files is located at wp-theme/vite/plugins/CopyFilePlugin.ts

import fs from 'fs';
import { resolve as resolvePath, dirname } from 'path';

import { Plugin } from 'vite';

export const CopyFilePlugin = ({
  sourceFileName,
  absolutePathToDestination,
}: {
  sourceFileName: string;
  absolutePathToDestination: string;
}): Plugin => ({
  name: 'copy-file-plugin',
  writeBundle: async (options: any, bundle) => {
    const fileToCopy = Object.values(bundle).find(({ name }) => name === sourceFileName);
    if (!fileToCopy) return;

    const sourcePath = resolvePath(options.dir, fileToCopy.fileName);

    await fs.promises.mkdir(dirname(absolutePathToDestination), { recursive: true });
    await fs.promises.copyFile(sourcePath, `${absolutePathToDestination}/${fileToCopy.fileName.replace('assets/', '')}`);
  },
});

which is exported in wp-theme/vite/plugins/index.ts

export { CopyFilePlugin } from './CopyFilePlugin';

The TypeScript/JS code is inside wp-theme/vite/ts with an index.ts entry file

import HelloWorld from './components/HelloWorld';
// @ts-expect-error
const $ = jQuery;

$(document).ready(() => {
  HelloWorld();
});

and here is the HelloWorld component used above – wp-theme/vite/ts/components/HelloWorld.ts

export default () => {
  console.log('Hello World');
}

The CSS as SASS is located in wp-theme/vite/scss/ where is an entry file style.ts with the next content

import './styles/style.scss';

And a styles directory located at wp-theme/vite/scss/styles where is a sass file style.scss with come css for testing

body {
  background:  #c0e97f;
}

Outside the vite folder that I covered in totality we have the wp-theme/functions.php file where we include the js and the css files generated by Vite

<?php

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

require_once get_theme_file_path('/inc/hmr.php');

define('THEME_PATH', get_template_directory_uri());

function enqueue_scripts_styles() {
	// manifest.json is empty for development
	$manifest = json_decode(file_get_contents(THEME_PATH . '/manifest.json'));
	if (!(array)$manifest) {
		// for development - load the JS as ES modules and the main sass file
		$handle = 'index';
		loadJSScriptAsESModule($handle);
		wp_enqueue_script($handle, getViteDevServerAddress() . '/ts/index.ts', array('jquery'), null);
		wp_enqueue_style('style', getViteDevServerAddress() . '/scss/styles/style.scss', null);
	} else { 
		//for production load the files from the manifest.json
		$js = 'index';
		$css = 'style.css';
		wp_enqueue_script($js, get_stylesheet_directory_uri() . $manifest->$js, array('jquery'), null);
		wp_enqueue_style($css, get_stylesheet_directory_uri() . $manifest->$css, array(), null);
	}
}

add_action('wp_enqueue_scripts', 'enqueue_scripts_styles', 20);

and in wp-theme/inc/hmr.php some usefull functions to help with the js files to be loaded as ES modules and to get the Vite dev server address

<?php
/**
 * Change the conditional to fit your needs.
 */
function getViteDevServerAddress() {
  if (str_contains($_SERVER['HTTP_HOST'], 'local')) {
    return 'http://localhost:1337';
  }
  return '';
}

function loadJSScriptAsESModule($script_handle) {
  add_filter(
    'script_loader_tag', function ($tag, $handle, $src) use ($script_handle) {
      if ($script_handle === $handle ) {
        return sprintf(
          '<script type="module" src="%s"></script>',
          esc_url($src)
        );
      }
      return $tag;
    }, 10, 3
  );
}

const VITE_HMR_CLIENT_HANDLE = 'vite-client';
function loadScript() {
  wp_enqueue_script(VITE_HMR_CLIENT_HANDLE, getViteDevServerAddress().'/@vite/client', array(), null);
  loadJSScriptAsESModule(VITE_HMR_CLIENT_HANDLE);
}
add_action('wp_enqueue_scripts', 'loadScript');

For this configuration some credit goes to this git repository https://github.com/milesaylward/wp-theme-vite-template that helped me achieve my way in using Vite in a WordPress theme.