Andrew Welch · #vite #webpack #frontend

Published , updated · 5 min read ·

Please consider 🎗 sponsoring me 🎗 to keep writing articles like this.

Vite.js Next Generation Frontend Tooling + Craft CMS

Vite.js is next gen­er­a­tion fron­tend tool­ing for mod­ern JavaScript that com­bines speed with sim­plic­i­ty. This arti­cle shows you how to inte­grate Vite.js with Craft CMS

Craft cms vite js

Vite.js is next gen­er­a­tion fron­tend tool­ing that can replace web­pack, Mix, Gulp, etc. for build­ing fron­tend assets like JavaScript and CSS.

In this arti­cle, we’ll explore how you can lever­age Vite.js as a fast, sim­ple build sys­tem for your Craft CMS websites.

Vite.js is a fast & opinionated… but its opinions are quite good

Vite.js is designed to work with the JavaScript & Node.js ecosys­tems, but using the Vite plu­g­in I wrote, we can use it for Craft CMS too.

Vite.js has fast start­up times because it lever­ages ES Mod­ules in mod­ern browsers rather than bundling, and it uses esbuild under the hood for fast tran­spi­la­tion of Type­Script, JSX, and more.

Link Why Vite.js?

The pri­ma­ry rea­sons to use Vite.js are speed and simplicity.

It’s fast to start up, the hot mod­ule replace­ment (HMR) is near-instan­ta­neous, and the pro­duc­tion build times are fast too.

It’s also quite sim­ple to set up, there isn’t much con­fig­u­ra­tion to be done, and using it involves just link­ing to the script you want to use.

Vite.js combines the simplicity of a script tag with the power of modern JavaScript

Instead of set­ting up an elab­o­rate Babel pipeline, you just put a <script src="/src/js/app.ts"> in your code, and like mag­ic you can use TypeScript.

While it is tough to pre­dict future trends, Vite.js also seems to be trend­ing in the right direc­tion rel­a­tive­ly to the new crop of bundling tools.

Parcel vs vite vs snowpack

par­cel vs vite vs snowpack

Link Configuring the Vite plugin for Craft CMS

Vite.js is JavaScript fron­tend tool­ing, so by default it uses an index.html as an entry­point to your appli­ca­tion.

That does­n’t make sense in the con­text of a serv­er-ren­dered set­up like Craft CMS, but with the Vite plu­g­in and some minor con­fig­u­ra­tion changes, it works like a charm.

Vite plugin logo

The first step is to install the plugin:

composer require nystudio107/craft-vite

You can also install the plu­g­in via the Plu­g­in Store in the Craft CP as well.

Next we need to copy the file config.php from vendor/nystudio107/craft-vite/src/ to the Craft config/ direc­to­ry as vite.php (so rename the file in the process).

Here’s what the file will look like:

 * Vite plugin for Craft CMS 3.x
 * Allows the use of the Vite.js next generation frontend tooling with Craft CMS
 * @link
 * @copyright Copyright (c) 2021 nystudio107

use craft\helpers\App;

 * Vite config.php
 * This file exists only as a template for the Vite settings.
 * It does nothing on its own.
 * Don't edit this file, instead copy it to 'craft/config' as 'vite.php'
 * and make your changes there to override default settings.
 * Once copied to 'craft/config', this file will be multi-environment aware as
 * well, so you can have different settings groups for each environment, just as
 * you do for 'general.php'

return [

    * @var bool Should the dev server be used?
    'useDevServer' => App::env('DEV_MODE'),

     * @var string File system path (or URL) to the Vite-built manifest.json
    'manifestPath' => '@webroot/dist/manifest.json',

     * @var string The public URL to the dev server (what appears in `<script src="">` tags
    'devServerPublic' => 'http://localhost:3000/',

     * @var string The internal URL to the dev server, when accessed from the environment in which PHP is executing
     *              This can be the same as `$devServerPublic`, but may be different in containerized or VM setups
    'devServerInternal' => 'http://vite:3000/',

     * @var string The public URL to use when not using the dev server
    'serverPublic' => App::env('SITE_URL') . '/dist/',

     * @var string The JavaScript entry from the manifest.json to inject on Twig error pages
     *              This can be a string or an array of strings
    'errorEntry' => '',

     * @var string String to be appended to the cache key
    'cacheKeySuffix' => '',

You’ll need to make a few changes to this file depend­ing on what your local devel­op­ment envi­ron­ment looks like.

  • useDevServer — is a boolean that sets whether you will be using Vite dev serv­er for hot mod­ule replace­ment (HMR). This defaults to using the DEV_MODE envi­ron­ment vari­able, so it’ll be on when in dev­Mode, and off when not.
  • manifestPath — the pub­lic serv­er path to your man­i­fest files; it can be a full URL or a par­tial path, or a Yii2 alias. This is usu­al­ly the same as what­ev­er you set your build.outDir to in vite.config.js
  • devServerPublic — the URL to the Vite dev serv­er, which is used for the hot mod­ule replace­ment (HMR); it can be a full URL or a par­tial path, or a Yii2 alias. Usu­al­ly this is http://localhost:3000, since Vite defaults to that. This will appear in <script> tags on the fron­tend when the dev serv­er is running
  • devServerInternal — the inter­nal URL to the Vite dev serv­er, which may be the same as devServerPublic or it may be dif­fer­ent if you’re using Dock­er or a VM. This is used by PHP to ping the dev serv­er to ensure it is run­ning; it can be a full URL or a par­tial path, or a Yii2 alias.
  • serverPublic — the pub­lic serv­er URL to your asset files; it can be a full URL or a par­tial path, or a Yii2 alias. This will appear in <script> tags on the fron­tend for pro­duc­tion builds. App::env('SITE_URL') . '/dist/' is a typ­i­cal setting
  • errorEntry — is a string, or array of strings, that should be the JavaScript entry point(s) (e.g.: /src/js/app.js) that should be inject­ed into Twig error tem­plates, to allow hot mod­ule replace­ment to work through Twig error pages. devMode must be true and useDevServer must also be true for this to have any effect.
  • cacheKeySuffix — String to be append­ed to the cache key

See the Vite doc­u­men­ta­tion if you need more help set­ting this up for your local dev environment.

Link Vite.js configuration

Next we need to set up Vite.js itself. First you’ll need a package.json that might look some­thing like this:

  "dependencies": {
    "vite-plugin-restart": "0.0.2"
  "devDependencies": {
    "@vitejs/plugin-legacy": "^1.3.3",
    "autoprefixer": "^10.2.5",
    "postcss": "^8.2.12",
    "tailwindcss": "^2.1.2",
    "vite": "^2.2.3"
  "name": "vite",
  "private": true,
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "serve": "vite preview"

These are the pack­ages you’ll need to set up a bare-bones Vite.js set­up that works with Tail­wind CSS. If you want to use Vue.js or React or Svelte or some oth­er fron­tend frame­work, you’ll just add those pack­ages and the appro­pri­ate Vite.js plu­g­ins.

Then do an npm install to install your pack­ages, and then gen­er­ate default tailwind.config.js and postcss.config.js files with:

npx tailwindcss init -p

Next we’ll cre­ate a vite.config.js file for our Vite.js configuration:

import legacy from '@vitejs/plugin-legacy'
import ViteRestart from 'vite-plugin-restart';

export default ({ command }) => ({
  base: command === 'serve' ? '' : '/dist/',
  build: {
    manifest: true,
    outDir: '/web/dist/',
    rollupOptions: {
      input: {
        app: '/src/js/app.ts',
  plugins: [
      targets: ['defaults', 'not IE 11']
      reload: [

The @vitejs/plugin-legacy plu­g­in gen­er­ates a lega­cy bun­dle for non-ESM native browsers, and the Vite plu­g­in takes care of gen­er­at­ing the appro­pri­ate module/​nomodule tags for you.

The vite-plu­g­in-restart plu­g­in allows us to get live reload for our Twig tem­plates or oth­er files, in addi­tion to the out of the box HMR for JavaScript and CSS. Just pass in as many path globs as you like.

Oth­er set­tings of note:

  • base — set to the root if the dev serv­er is run­ning, and oth­er­wise set it to /dist/ so our built assets are in their own direc­to­ry (and often not checked into Git).
  • build.manifest — set to true so that the Rollup build will gen­er­ate a man­i­fest file of the pro­duc­tion assets
  • build.outDir — spec­i­fies where the built pro­duc­tion assets should go, as a file sys­tem path rel­a­tive to the vite.config.js file.
  • build.rollupOptions.input — set to an object that has key/​value pairs for each of our entry­point scripts (need­ed since we’re not using an index.html as our appli­ca­tion entry­point). These should be the full path to the script as ref­er­enced in your Twig code

Then to start up the Vite.js dev serv­er, you just do:

npm run dev

Your JavaScript files will be served up from http://localhost:3000 while the dev serv­er is running.

When it’s time to do a pro­duc­tion build, you just do:

npm run build

And your pro­duc­tion assets will be built for your via Rollup, ready to deploy.

For more infor­ma­tion on Vite.js itself, check out the excel­lent Vite.js doc­u­men­ta­tion.

Link Using Vite.js with Craft CMS

Cre­ate your JavaScript, Type­Script, Vue sin­gle file com­po­nents, etc. as you nor­mal­ly would. Let’s say we store them in the direc­to­ry /src/js/

Here’s an exam­ple Type­Script file (remem­ber, it could be JavaScript too):

import "vite/dynamic-import-polyfill";
import '/src/css/app.pcss';

console.log("Hello, world!");

Two things to note here:

  1. We need to import the vite/­dy­nam­ic-import-poly­fill file into in our build.input JavaScript file entries list­ed in the vite.config.js
  2. Any CSS that we want processed via PostC­SS needs to be import­ed via JavaScript. We don’t include the CSS in our tem­plates via <link rel="stylesheet"> tags, the plu­g­in does it for us.

That’s it. Then in our Twig tem­plates, we just need to ref­er­ence the file via a Twig func­tion pro­vid­ed by the Vite plugin:

{{ craft.vite.script("/src/js/app.ts") }}

In devel­op­ment with the Vite.js dev serv­er run­ning, this will gen­er­ate a tag that looks like this:

<script type="module" src="http://localhost:3000/src/js/app.ts"></script>

The CSS will be inject­ed auto­mat­i­cal­ly into the DOM via inline <style> tags while the dev serv­er is running.

In pro­duc­tion with­out the Lega­cy plu­g­in, the Twig func­tion will gen­er­ate the fol­low­ing tags:

<script type="module" src="" crossorigin></script>
<link href="" rel="stylesheet" media="print" onload="'all'">

Since we’re using the Lega­cy plu­g­in for dual mod­ern + lega­cy builds, we’ll get even more tags gen­er­at­ed automatically:

    !function(){var e=document,t=e.createElement("script");if(!("noModule"in t)&&"onbeforeload"in t){var n=!1;e.addEventListener("beforeload",function(e){if(!0;else if(!"nomodule")||!n)return;e.preventDefault()},!0),t.type="module",t.src=".",e.head.appendChild(t),t.remove()}}();
<script type="nomodule" src=""></script>
<script type="module" src="" crossorigin></script>
<link href="" rel="stylesheet" media="print" onload="'all'">
<script type="nomodule" src=""></script>

This includes:

  • Safari 10.1 nomod­ule fix script
  • Lega­cy nomodule poly­fills for dynam­ic imports for old­er browsers
  • Mod­ern app.js mod­ule for mod­ern browsers
  • Extract­ed CSS, loaded asyn­chro­nous­ly by default
  • Lega­cy app.js script for lega­cy browsers

This is the module/​nomodule pat­tern that’s been in use for years to allow for mod­ern and lega­cy JavaScript bundles.

Mod­ules that are dynam­i­cal­ly loaded via one of your entry scripts will appear in the brows­er as <link rel="modulepreload"> — check out the Pre­load­ing Mod­ules for infor­ma­tion on that.

Link Live Reload through Twig Errors

The Vite plu­g­in includes sup­port for live reload­ing through Twig error pages. You just need to set the errorEntry set­ting in the config/vite.php file, e.g.:

    'errorEntry' => '/src/js/app.ts',

Then the page will auto-reload when­ev­er you fix the error!

Link Bundling it all up

As Evan You, the cre­ator of Vite.js (and Vue.js) said:

It is NOT Vite’s goal to com­plete­ly replace web­pack. There are prob­a­bly a small num­ber of features/​capabilities that some exist­ing web­pack projects rely on that does­n’t exist in Vite, but those fea­tures are in the long tail and are only need­ed by a small num­ber of pow­er users who write bespoke web­pack con­fig­u­ra­tion.

How­ev­er, in the con­text of the gen­er­al web dev pop­u­la­tion, 90% of exist­ing devs and 100% of begin­ners don’t need or care about these long tail fea­tures.

So — try it, and if it does­n’t work for you, stick to webpack.

I think Vite.js is com­pelling enough that it’s def­i­nite­ly worth giv­ing it a whirl. The speed and sim­plic­i­ty real­ly are remarkable.

If you’re a Craft CMS plu­g­in devel­op­er, check out the nys­tu­dio107/craft-plu­g­in-vite pack­age as a way to inte­grate a Vite.js build­chain into your plugins.

If you want to learn more, check out the Vite Awe­some page for more, well, awe­some Vite.js plu­g­ins, inte­gra­tions, and tutorials.

Hap­py bundling!