Skip to content

Scrutinizer Code Quality Code Coverage Build Status Code Intelligence Status

Vite plugin for Craft CMS

Allows the use of the Vite.js next generation frontend tooling with Craft CMS

Screenshot

Related Article: Vite.js Next Generation Frontend Tooling + Craft CMS

Requirements

This plugin requires Craft CMS 5.0.0 or later

Installation

To install the plugin, follow these instructions.

  1. Open your terminal and go to your Craft project:

     cd /path/to/project
    
  2. Then tell Composer to load the plugin:

     composer require nystudio107/craft-vite
    
  3. In the Control Panel, go to Settings → Plugins and click the “Install” button for Vite.

  4. Install Vite. Vite’s Getting Started Guide provides various installation options.

Vite Overview

Vite is a bridge between Craft CMS/Twig and the next generation frontend build tool Vite.js

Vite allows for Hot Module Replacement (HMR) of JavaScript, CSS, and Twig (even through errors) during development, as well as optimized production builds.

Vite supports both modern and legacy bundle builds, as per the Deploying ES2015+ Code in Production Today article.

Vite also handles generating the necessary <script> and <link> tags to support both synchronous and asynchronous loading of JavaScript and CSS.

Additionally, Vite has a caching layer to ensure optimal performance.

Configuring Vite

Vite.js is JavaScript frontend tooling, so by default it uses an index.html as an entrypoint to your application, but with the Vite plugin and some minor configuration changes, we can use it with server-rendered setups like Craft CMS.

Configuration for Vite is done via the config.php config file. Here’s the default config.php; it should be renamed to vite.php and copied to your config/ directory to take effect.

The config.php File

php
<?php

use craft\helpers\App;

return [
    'useDevServer' => App::env('ENVIRONMENT') === 'dev' || App::env('CRAFT_ENVIRONMENT') === 'dev',
    'manifestPath' => '@webroot/dist/manifest.json',
    'devServerPublic' => 'http://localhost:3000/',
    'serverPublic' => App::env('PRIMARY_SITE_URL') . '/dist/',
    'errorEntry' => '',
    'cacheKeySuffix' => '',
    'devServerInternal' => '',
    'checkDevServer' => false,
    'includeReactRefreshShim' => false,
    'includeModulePreloadShim' => true,
    'criticalPath' => '@webroot/dist/criticalcss',
    'criticalSuffix' =>'_critical.min.css',
];

These are the settings you’ll need to change for your project:

  • useDevServer - is a boolean that sets whether you will be using Vite dev server for hot module replacement ( HMR). If this is set to false, the files will be pulled from the manifest.json specified in manifestPath
  • manifestPath - the public server path to your manifest files; it can be a full URL or a partial path, or a Yii2 alias. This is usually the same as whatever you set your build.outDir to in vite.config.js
  • devServerPublic - the URL to the Vite dev server, which is used for the hot module replacement (HMR); it can be a full URL or a partial path, or a Yii2 alias. Usually this is http://localhost:3000, since Vite defaults to that. This will appear in <script> tags on the frontend when the dev server is running
  • serverPublic - the public server URL to your asset files; it can be a full URL or a partial path, or a Yii2 alias. This will appear in <script> tags on the frontend for production builds. App::env('PRIMARY_SITE_URL') . '/dist/' is a typical setting

These are completely optional settings that you probably won’t need to change:

  • errorEntry - is a string, or array of strings, that should be the JavaScript entry point(s) (for example: src/js/app.ts) in your manifest.json that should be injected into Twig error templates, to allow hot module replacement 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 appended to the cache key
  • devServerInternal - 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. ONLY used if $checkDevServer = true
  • checkDevServer - Should we check for the presence of the dev server by pinging $devServerInternal to make sure it’s running?
  • includeReactRefreshShim - whether or not the required shim for react-refresh should be included when the Vite dev server is running
  • includeModulePreloadShim - whether or not the shim for modulepreload-polyfill should be included to polyfill <link rel="modulepreload">

If you’re using the rollup-plugin-critical to generate critical CSS, use these settings:

  • criticalPath - File system path (or URL) to where the Critical CSS files are stored
  • criticalSuffix - the suffix added to the name of the currently rendering template for the critical CSS filename

Note also that the manifestPath defaults to a Yii2 alias @webroot/dist/manifest.json (adjust as necessary to point to your manifest.json on the file system); this allows Vite to load the manifest from the file system, rather than via http request, and is the preferred method. However, it works fine as a full URL as well if you have your manifest.json hosted on a CDN or such.

Configuring Vite.js

Vite 5.0 or later

As of Vite 5.0 or later, there’s been a change to where the manifest.json is generated by default.

It is now placed inside a .vite/ directory by default. You can accommodate this in either of the following ways:

  • Change the manifest setting in your vite.config.js file to manifest.json (it can not either be a bool or a file path string)
  • Change the manifestPath setting in your config/vite.php file to '@webroot/dist/.vite/manifest.json'

Basic Config

Here’s a basic vite.config.js for use as a Vite config:

js
export default ({command}) => ({
  base: command === 'serve' ? '' : '/dist/',
  build: {
    manifest: true,
    outDir: '../cms/web/dist/',
    rollupOptions: {
      input: {
        app: 'src/js/app.ts',
      }
    },
  },
});
  • base - set to the root if the dev server is running, and otherwise set it to /dist/ so our built assets are in their own directory (and often not checked into Git).
  • build.manifest - set to true so that the Rollup build will generate a manifest file of the production assets
  • build.outDir - specifies where the built production assets should go, as a file system path relative to the vite.config.js file.
  • build.rollupOptions.input - set to an object that has key-value pairs for each of our entrypoint scripts (needed since we’re not using an index.html as our application entrypoint). These should be the full path to the script as referenced in your Twig code

Specifying the dev server port

Prior to version 3, Vite used port 3000 by default for the dev server. Now it uses port 5137 by default, so we can specify the port we want to use specifically:

js
export default ({command}) => ({
  base: command === 'serve' ? '' : '/dist/',
  build: {
    manifest: true,
    outDir: '../cms/web/dist/',
    rollupOptions: {
      input: {
        app: 'src/js/app.ts',
      }
    },
  },
  server: {
    fs: {
      strict: false
    },
    origin: 'http://localhost:3000',
    port: 3000,
    strictPort: true,
  }
});

Modern + Legacy Config

By default, Vite generates JavaScript bundles that work with modern browsers that support native ESM.

If you require support for legacy browsers, you can use the @vitejs/plugin-legacy plugin:

js
import legacy from '@vitejs/plugin-legacy'

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

This will generate -legacy files for your production bundles, and the Vite plugin automatically detects them and uses the module/nomodule pattern for you.

Entry Script HMR

Your entry scripts are what you list in the build.rollupOptions.input section of the config.

Vite is typically used with frameworks which take care of the HMR via the dev server. However, to get your entry scripts to HMR you need to add this snippet to each entry script:

js
// Accept HMR as per: https://vitejs.dev/guide/api-hmr.html
if (import.meta.hot) {
  import.meta.hot.accept(() => {
    console.log("HMR")
  });
}

If you don’t do the above, you’ll get a full page reload whenever you modify one of your entry scripts.

Anything your scripts import will be automatically HMR’d by the dev server.

Live Reload of Twig Config

Vite provides hot module replacement (HRM) of CSS and JavaScript as you build your application out of the box.

If you want live reload of your Twig (or other) files as you develop, you can get that with the vite-plugin-restart plugin:

js
import ViteRestart from 'vite-plugin-restart';

// https://vitejs.dev/config/
export default ({command}) => ({
  base: command === 'serve' ? '' : '/dist/',
  build: {
    manifest: true,
    outDir: '../cms/web/dist/',
    rollupOptions: {
      input: {
        app: 'src/js/app.ts',
      }
    },
  },
  plugins: [
    ViteRestart({
      reload: [
        '../cms/templates/**/*',
      ],
    }),
  ],
});
  • plugins.ViteRestart.reload - lets you specify and array of file system paths or globs to watch for changes, and issue a page refresh if any of the files change

The Vite plugin has support for enabling live refresh even through Twig error pages as you develop.

Local Development Environment Setup

Using HTTPS

If you’re using TLS (https) in local dev, you may get mixed content errors if you don’t change your devServerPublic to https (and you’d need change your server.HTTPS Vite config too).

Then you’ll want to ensure you have a trusted self-signed certificate for your browser, or you can use the vite-plugin-mkcert plugin.

Using Laravel Valet

If you’re using Laravel Valet you may run into issues with https. If so, you can add this config to your vite.config.js (see below):

  server: {
  https: {
    key: fs.readFileSync('localhost-key.pem'),
    cert: fs.readFileSync('localhost.pem'),
  },
  hmr: {
    host: 'localhost',
  }
}

Using Homestead/VM

If you’re using a VM like Homestead, to get Hot Module Replacement (HMR) working, you’ll need to add this config to your vite.config.js (see below):

  server: {
  host: '0.0.0.0',
    watch: {
    usePolling: true,
  },
}

To work inside of a VM like VirtualBox (which is typically used by Homestead) you need to enable polling.

Using Docker

To work properly with a Docker setup, the server.host needs to be set to 0.0.0.0 so that it broadcasts to all available IPv4 addresses in your vite.config.js (see below):

  server: {
    host: '0.0.0.0',
  }

Using DDEV

To run Vite inside a DDEV container, you’ll have to define a custom service that proxies requests from the frontend to the vite server running inside the VM. This is done by creating a /.ddev/docker-compose.*.yaml file, and exposing an additional port to your project.

Create a file named docker-compose.vite.yaml and save it in your project’s /.ddev folder, with the following contents:

yaml
# Override the web container's standard HTTP_EXPOSE and HTTPS_EXPOSE services
# to expose port `3000` of DDEV's web container.
version: '3.6'
services:
  web:
    expose:
      - '3000'
    environment:
      - HTTP_EXPOSE=${DDEV_ROUTER_HTTP_PORT}:80,${DDEV_MAILPIT_PORT}:8025,3001:3000
      - HTTPS_EXPOSE=${DDEV_ROUTER_HTTPS_PORT}:80,${DDEV_MAILPIT_HTTPS_PORT}:8025,3000:3000

In your vite.config.js, the server.host should to be set to 0.0.0.0, and server.port set to 3000:

server: {
  host: '0.0.0.0',
  port: 3000
}

With the above set up, Craft Vite will now have access to the devServerInternal via http://localhost:3000, and devServerPublic via https://projectname.ddev.site:3000. Note that devServerPublic can run over http or https, devServerInternal is always http. Your config/vite.php file might thus look like:

php
<?php

use craft\helpers\App;

return [
	'checkDevServer' => true,
	'devServerInternal' => 'http://localhost:3000',
	'devServerPublic' => App::env('PRIMARY_SITE_URL') . ':3000',
	'serverPublic' => App::env('PRIMARY_SITE_URL') . '/dist/',
	'useDevServer' => App::env('ENVIRONMENT') === 'dev' || App::env('CRAFT_ENVIRONMENT') === 'dev',
	// other config settings...
];

If you’re using the rollup-plugin-critical to generate critical CSS, you must add extra Debian packages to enable Puppeteer Headless Chrome support. Add the following line to your /.ddev/config.yaml file:

yaml
webimage_extra_packages: [ gconf-service, libasound2, libatk1.0-0, libcairo2, libgconf-2-4, libgdk-pixbuf2.0-0, libgtk-3-0, libnspr4, libpango-1.0-0, libpangocairo-1.0-0, libx11-xcb1, libxcomposite1, libxcursor1, libxdamage1, libxfixes3, libxi6, libxrandr2, libxrender1, libxss1, libxtst6, fonts-liberation, libappindicator1, libnss3, xdg-utils ]

For Apple Silicon support, you will have to override that configuration by adding a config.m1.yaml file in your ddev folder along with the config.yaml one with the following content:

yaml
webimage_extra_packages : [chromium]
web_environment:
- CPPFLAGS=-DPNG_ARM_NEON_OPT=0
- PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
- PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true

Then be sure to set criticalUrl to http://localhost as part of your rollup configuration.

Finally note that as of DDEV 1.19 you are able to specify Node.js (and Composer) versions directly via /.ddev/config.yaml. See more at https://ddev.readthedocs.io/en/stable/users/cli-usage/#nodejs-npm-nvm-and-yarn

Vite-Processed Assets

This is cribbed from the Laravel Vite integration docs:

There is currently an unsolved issue when referencing assets in files processed by Vite, such as a Vue or CSS file. In development, URLs will not be properly rewritten.

Additionally, there is currently no way to get the path of a Vite-processed asset (for example an image that was imported in a Vue SFC) from the back-end, since the manifest does not reference the original file path. In most cases, this should not be an issue, as this is not a common use case.

What you can do is leverage the /public Public Directory for static assets in Vite, so the URLs will not get rewritten.

The basic problem is if you have a CSS rule like:

background-image: url('/src/img/woof.jpg');

...and your local dev runs off of something like myhost.test then the image will be referenced as:

/src/img/woof.jpg

...which then resolves to the current host:

http://myhost.test/src/img/woof.jpg

...when what you really want is for it to be coming from the Vite dev server:

http://localhost:3000/src/img/woof.jpg

This is only a problem when you’re using Vite with a backend system like Craft CMS, where the host you run the site from is different from where the Vite dev server runs.

To work around this, as of Vite ^2.6.0 you can use the server.origin config to tell Vite to serve the static assets it builds from the Vite dev server, and not the site server:

  server: {
  origin: 'http://localhost:3000',
  host: '0.0.0.0',
}

This issue was discussed in detail, and fixed via a pull request that was rolled into Vite ^2.6.0 in the form of the origin setting.

Other Config

Vite uses esbuild so it is very fast, and has built-in support for TypeScript and JSX.

Vite also comes with support for a number of plugins that allow you to use:

You can easily use Tailwind CSS with Vite as well, with or without the JIT.

Check out Awesome Vite for other great Vite plugins & resources.

Using Vite

The .script() function

The script() function is the primary tag you will use with Vite. It outputs the JavaScript and CSS tags generated by for your input JavaScript files.

JavaScript

Once the Vite plugin is installed and configured, using it is quite simple. Where you would normally link to a JavaScript file via Twig in a <script> tag, you instead do:

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

Vite automatically supports the direct linking to TypeScript (as in the above example), JavaScript, JSX, and other files formats via plugins. You just link directly to them, that’s it.

CSS is Asynchronous by default

By default, the Vite plugin will load your CSS asynchronously. If you are not using Critical CSS, you’ll want to change that to avoid Flash Of Unstyled Content (FOUC) by setting the setting the second argument to false:

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

See the Other Options section below for details

Development

If the Vite dev server is running, this will cause it to output something like this:

html

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

This is causing it to get the script file from the Vite dev server, with full hot module replacement (HMR) support.

Production

In production or otherwise where the Vite dev server is not running, the output will look something like this:

html

<script type="module" src="https://example.com/dist/assets/app.56c9ea9d.js" crossorigin></script>
<link href="https://example.com/dist/assets/app.c30f6458.css" rel="stylesheet" media="print" onload="this.media='all'">
Module Preload tags

The Vite plugin will also generate <link rel="modulepreload"> tags for any script modules that your script output via craft.vite.script() imports. The tags will look like this:

html

<link href="http://localhost:8000//dist/assets/vendor.a785de16.js" rel="modulepreload" crossorigin>

Preloading helps with performance by telling the browser about what it needs to fetch so that it’s not stuck with nothing to do during those long roundtrips.

Sub-Resource Integrity

If you use a Vite plugin such as vite-plugin-manifest-sri, the Craft Vite plugin will include subresource integrity attributes for the <script type="module"> & <link rel="modulepreload"> tags that it generates as well. For example:

html

<script type="module" src="http://localhost:3000/src/js/app.ts"
        integrity="oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"></script>

Subresource Integrity (SRI) is a security feature that enables browsers to verify that resources they fetch are delivered without unexpected manipulation. It works by allowing you to provide a cryptographic hash that a fetched resource must match.

N.B.: If you use a service such as CloudFlare with Auto Minify enabled, this alters the payload that is being delivered dynamically, which will cause the SRI check to fail, and your files will not load.

The same is true for any modifications to the built JavaScript. External changes to the built files is exactly what SRI is designed to thwart. So you will need to either disable any options that dynamically alter your built files, or don’t use SRI.

Script onload events

The <script> tags generated by Vite will automatically fire a vite-script-loaded event dispatched to document so listeners can be notified after a script loads.

If you have JavaScript that needs to be executed after one of your scripts are loaded, you can listen for this event:

js
document.addEventListener('vite-script-loaded', function (e) {
  if (e.detail.path === 'src/js/app.ts') {
    // The script with the path src/js/app.ts is now loaded, do whatever initialization is needed
  }
});

CSS

To use CSS with Vite, you must import it in one of your build.input JavaScript file entries listed in the vite.config.js, for example:

js
import '/src/css/app.pcss';

The Vite plugin will take care of automatically generating the <link rel="stylesheet"> tag for you in production.

By default, it loads the CSS asynchronously, but you can configure this. See the Other Options section.

Legacy

If you’re using the vite-plugin-legacy plugin to generate builds compatible with older browsers, the Vite plugin will automatically detect this and use the module/nomodule pattern when outputting production build tags.

The Twig code:

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

Would then result in the following output, depending on whether the Vite dev server is running:

Development

If the Vite dev server is running, this will cause it to output something like this:

html

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

This is causing it to get the script file from the Vite dev server, with full hot module replacement (HMR) support.

Production

In production or otherwise where the Vite dev server is not running, the output will look something like this:

html

<script>
  !function () {
    var e = document, t = e.createElement("script");
    if (!("noModule" in t) && "onbeforeload" in t) {
      var n = !1;
      e.addEventListener("beforeload", function (e) {
        if (e.target === t) n = !0; else if (!e.target.hasAttribute("nomodule") || !n) return;
        e.preventDefault()
      }, !0), t.type = "module", t.src = ".", e.head.appendChild(t), t.remove()
    }
  }();
</script>
<script type="nomodule" src="https://example.com/dist/assets/polyfills-legacy.8fce4e35.js"></script>
<script type="module" src="https://example.com/dist/assets/app.56c9ea9d.js" crossorigin></script>
<link href="https://example.com/dist/assets/app.c30f6458.css" rel="stylesheet" media="print" onload="this.media='all'">
<script type="nomodule" src="https://example.com/dist/assets/app-legacy.0c84e934.js"></script>

So that includes:

  • Safari 10.1 nomodule fix script
  • Legacy nomodule polyfills for dynamic imports for older browsers
  • Modern app.js module for modern browsers
  • Extracted CSS
  • Legacy app.js script for legacy browsers

The .register() function

In addition to the craft.vite.script() function, the Vite plugin also provides a .register() function:

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

This works exactly the way the .script() function works, but instead of outputting the tags, it registers them with the Craft::$app->getView().

This is primarily useful in plugins that must exist inside of the CP, or other things that leverage the Yii2 AssetBundles and dependencies.

The .entry() function

The Vite plugin includes an .entry() function that retrieves the URL to the entry in the manifest.json file. This function will not ever return a URL from the devServer.

You pass in a relative path to the entry, just as you do for JavaScript files in Vite. For example:

twig
    {{ craft.vite.entry("app.css") }}

This will return a URL like this in regardless of whether the devServer is running or not:

http://localhost:8000/dist/assets/app.66b94608.css

The .asset() function

The Vite plugin includes a .asset() function that retrieves an asset served via Vite in your templates. Assets served from Vite include images or fonts that are referenced via CSS, or are imported via JavaScript.

You pass in a relative path to the asset, just as you do for JavaScript files in Vite. For example:

twig
    {{ craft.vite.asset("src/images/quote-open.svg") }}

This will return a URL like this when the Vite dev server is running:

html
http://localhost:3000/src/assets/img/quote-open.svg

...and a URL like this in production when the Vite dev server is not running:

http://localhost:8000/dist/assets/quote-open.66b94608.svg

N.B.: this is only for assets referenced via CSS or imported via JavaScript. For other static assets, you can either put them in the Vite public directory or you can use the rollup-plugin-copy to copy the assets into your public dist/ directory:

javascript
import copy from 'rollup-plugin-copy';

export default ({command}) => ({
  plugins: [
    copy({
      targets: [{src: 'src/fonts/**/*', dest: '../cms/web/dist/fonts'}],
      hook: 'writeBundle'
    }),
  ]
});

To access assets that are in the public/ directory from Twig, there is a second parameter you can pass to craft.vite.asset to inidicate that the asset is coming from the public/ directory:

twig
    {{ craft.vite.asset("src/images/quote-open.svg", true) }}

Using craft.vite.asset with CSS

If you are using Vite 3.x or later, you can also use craft.vite.asset to manually include CSS that is a top-level entry in your vite.config.js (rather than being imported into your JavaScript):

twig
    {{ craft.vite.asset("src/css/app.css") }}

This assumes your vite.config.js looks something like this:

js
  build: {
    emptyOutDir: true,
    manifest: true,
    rollupOptions: {
      input: {
        app: 'src/js/app.ts',
        css: 'src/css/app.css'
      },
      output: {
        sourcemap: true
      },
    }
  },

The .inline() function

The Vite plugin also includes a .inline() function that inlines the contents of a local file (via path) or remote file (via URL) in your templates.

Yii2 aliases and/or environment variables may be used, and a caching layer is used so that remote files will be kept in the cache until it is cleared, for performance reasons.

URL example:

twig
    {{ craft.vite.inline("https://example.com/my-file.txt") }}

Path example:

twig
    {{ craft.vite.inline("@webroot/my-file.txt") }}

The .devServerRunning() function

The Vite plugin has a .devServerRunning() function that allows you to determine if the Vite dev server is running from your Twig templates.

For instance, you could do:

twig
{% if craft.vite.devServerRunning() %}
   <base href="{{ alias('@viteBaseUrl') }}">
{% endif %}

To side-step the Vite Processed Assets issue.

The .includeCriticalCssTags() function

The Vite plugin includes a .includeCriticalCssTags() function that will look for a file in criticalPath that matches the name of the currently rendering template, with criticalSuffix appended to it.

Used in combination with the rollup-plugin-critical plugin, this automates the inclusion of critical CSS. For example:

twig
    {{ craft.vite.includeCriticalCssTags() }}

To pass in your own path to a specific critical CSS file, specify the template path you used to generate that file (see criticalPages in the vite.config.js in the rollup-plugin-critical).

twig
    {{ craft.vite.includeCriticalCssTags("homepage/index") }}

...and you can also pass in attributes to be added to the <style> tag as well:

twig
    {{ craft.vite.includeCriticalCssTags(null, {
         'data-css-info': 'bar',
    }) }}

If null is passed in as the first parameter, it’ll use the automatic template matching to determine the filename.

The .getCssInlineTags() function

To inline an existing CSS file by URL or path, you do it by path:

twig
    {{ craft.vite.getCssInlineTags("@webroot/path/to/css/file.css") }}

Or by URL (but keep in mind, this will generate an additional blocking XHR):

twig
    {{ craft.vite.getCssInlineTags("@web/path/to/css/file.css") }}

...and you can also pass in attributes to be added to the <style> tag as well:

twig
    {{ craft.vite.getCssInlineTags("@web/path/to/css/file.css", {
         'data-css-info': 'bar',
    }) }}

The .getCssHash() function

Pass in the path to your entrypoint script, and it will return the hash of the CSS asset:

twig
   {% set cssHash = craft.vite.getCssHash("src/js/app.ts") %}

If the CSS file in the manifest has the name app.245485b3.css, the above function will return 245485b3.

This can be used for critical CSS patterns, for example:

twig
{# -- Critical CSS -- #}
{#
# Use Nginx Server Sider Includes (SSI) to render different HTML depending on
# the value in the `critical-css` cookie. ref: http://nginx.org/en/docs/http/ngx_http_ssi_module.html
#}
{% set cssHash = craft.vite.getCssHash("src/js/app.ts") %}
{#
 # If the `critical-css` cookie is set, the client already has the CSS file download,
 # so don't include the critical CSS, and load the full stylesheet(s) synchronously
 #}
<!--# if expr="$HTTP_COOKIE=/critical\-css\={{ cssHash }}/" -->
{{ craft.vite.script("src/js/app.ts", false) }}
<!--# else -->
{#
# If the cookie is not set, set the cookie, then include the critical CSS for this page,
# and load the full stylesheet(s) asychronously
#}
<script>
     Cookie.set("critical-css", "{{ cssHash }}", { expires: "7D", secure: true });
</script>
{{ craft.vite.includeCriticalCssTags() }}
{{ craft.vite.script("src/js/app.ts", true) }}
<!--# endif -->

Other Options

The .script() and .register() functions accept additional options as well:

twig
    {{ craft.vite.script(PATH, ASYNC_CSS, SCRIPT_TAG_ATTRS, CSS_TAG_ATTRS) }}
  • PATH - string - the path to the script
  • ASYNC_CSS - boolean - whether any CSS should be loaded async or not (defaults to true)
  • SCRIPT_TAG_ATTRS - array - an array of key-value pairs for additional attributes to add to any generated script tags
  • CSS_TAG_ATTRS - array - an array of key-value pairs for additional attributes to add to any generated CSS link tags

So for example:

twig
    {{ craft.vite.script(
        "src/js/app.ts",
        false,
        { 'data-script-info': 'foo' },
        { 'data-css-info': 'bar' },
    ) }}

Craft Cloud

During a build, Craft Cloud deploys static assets to a CDN, so you’ll need to configure the plugin to use the appropriate URLs:

php
<?php
// config/vite.php

use craft\cloud\Helper as CloudHelper;

return [
    'manifestPath' => CloudHelper::artifactUrl('dist/.vite/manifest.json'),
    'serverPublic' => CloudHelper::artifactUrl('dist/'),
];

This helper function returns a CDN URL that includes your project and environment identifiers, like this:

https://cdn.craft.com/{project-uuid}/builds/{environment-uuid}/artifacts/dist/

Outside of Cloud, this behaves as though it were prepended with @web. If you’d prefer to use an on-disk manifestPath when working locally (instead of a URL), the CloudHelper::isCraftCloud() function lets you switch based on the environment:

php
<?php
// config/vite.php

use craft\cloud\Helper as CloudHelper;

return [
    'manifestPath' => CloudHelper::isCraftCloud() ? CloudHelper::artifactUrl('dist/.vite/manifest.json') : '@webroot/dist/.vite/manifest.json',
    'serverPublic' => CloudHelper::artifactUrl('dist/'),
];

Additionally, your Vite config should have public base set to use the same CDN URL. In Craft Cloud’s build pipeline, this is exposed as an CRAFT_CLOUD_ARTIFACT_BASE_URL environment variable:

javascript
// vite.config.js
export default ({ command }) => ({
  base: command === 'serve' ? '' : `${process.env.CRAFT_CLOUD_ARTIFACT_BASE_URL || ''}/dist/`,
})

Vite Roadmap

Some things to do, and ideas for potential features:

Brought to you by nystudio107