Nette Vite – using Nette with Vite for rapid local development

4 months ago by Lubomír Blažek translated by David Grudl  

Vite is a next-generation frontend tool for loading JS and CSS files directly from sources, without the need for compilation and using modern standards.

I'm sure someone is annoyed that when they make an edit on the frontend, they have to compile and wait unnecessarily, and then run the compilation over and over every time they make an edit. In addition, the system often needs to be configured in a complex way. Fortunately, this is over thanks to modern tools and approaches.

JavaScript modules can be loaded in many ways, the most well-known being CommonJS, which you find in Node.js. However, this won't work in the browser unless you use a library like RequireJS.

A modern and standardized format that works in both Node.js and the browser is ES Modules (ESM). Theoretically, you can write source code with ES modules and using importmap, which will work in browsers once without having to compile anything. So far only Chrome supports this, for other browsers you need to use es-module-shim.

Vite takes a similar approach and uses ES modules exclusively – but without importmap for now. It bundles dependencies using esbuild. This is written in Go and builds 10–100× faster than other tools. The local server then transforms the paths to the modules so that the browser understands them. Currently, most libraries are still written in CommonJS, so Vite transforms those into ESM syntax so they can be loaded by the browser. It then bundles the dependencies on the fly and only when needed. There is no need to compile your own code, it is already written in a modern ES format that the browser understands.

// source file
import { Application, Controller } from "@hotwired/stimulus";

For the record, with import-maps you can load the source code as is, e.g. from the CDN esm.sh. This is an alternative approach but it is not yet supported by Vite.

// loaded file in the browser (via Vite)
import { Application, Controller } from "/node_modules/.vite/@stimulus_core.js?v=02a6d100";

So you load JavaScript directly from the source files in the project and there is no need to compile anything. Vite handles this on demand, and with Hot Module Replacement (HMR) only the changed files are compiled. Vite has to run in the background, but if you're using local development, that's not a problem. It can even be used on a remote server, where CSS and JS are loaded from the localhost.

For loading sources, we'll create a simple class to distinguish whether to load source files (development) or build files (production):

use Nette\Utils\FileSystem;
use Nette\Utils\Html;
use Nette\Utils\Json;


class ViteAssets
{
    public function __construct(
        private string $viteServer,
        private string $manifestFile,
        private bool $productionMode,
    ){}

    public function printTags(string $entrypoint): void
    {
        $scripts = [];
        $styles = [];
        $baseUrl = '/';

        if ($this->productionMode) {
            if (file_exists($this->manifestFile)) {
                $manifest = Json::decode(FileSystem::read($this->manifestFile), Json::FORCE_ARRAY);
                $scripts = [$manifest[$entrypoint]['file']];
                $styles = $manifest[$entrypoint]['css'] ?? [];
            } else {
                trigger_error('Missing manifest file: ' . $this->manifestFile, E_USER_WARNING);
            }

        } else {
            $baseUrl = $this->viteServer . '/';
            $scripts = ['@vite/client', $entrypoint];
        }

        foreach ($styles as $path) {
            echo Html::el('link')->rel('stylesheet')->href($baseUrl . $path);
        }

        foreach ($scripts as $path) {
            echo Html::el('script')->type('module')->src($baseUrl . $path);
        }
    }
}

Simply configure the class and pass it to the presenter:

services:
	- ViteAssets(http://localhost:3000, %wwwDir%/manifest.json, not(%debugMode%))
abstract class BasePresenter extends Nette\Application\UI\Presenter
{
	public function __construct(
		private ViteAssets $viteAssets,
	) {
	}

    public function beforeRender(): void
    {
		$this->template->viteAssets = $this->viteAssets;
    }
}

And use it in the layout template:

{$viteAssets->printTags('src/main.js')}

I created an example of Nette integration with Vite on GitHub, based on web-project.

I've added a Tracy extension for Vite, where you can use an icon to toggle loading via Vite, based on a cookie stored in the browser.

For local development, there is an added Docker Image by Josef Jebavy. So you just need to have Docker and Node.js 14+ installed and you don't actually need PHP and Composer on your system. Then I added the following libraries as a starter pack for the front-end.

  • Tailwind – utility classes for everything
  • Stimulus – for bind events and controllers
  • Turbo – automatic navigation between pages without refresh, further it can be used to deal with live loading of content or redrawing of snippets

First, install the Node.js package dependencies using npm i. After installation, start the local web server with docker compose up, start Vite with npm run dev.

Web Sockets are also used with Vite, so any change in .php, .latte or .css, .js files will automatically reload the browser and retrieve the current data.

For production, it then runs npm run build which builds the source JS and CSS files into www/assets and adds manifest.json to www where the revisions of each file are.

It's actually a very similar approach to Laravel Vite

Vite is configured in the vite.config.js file of the project and the basic configuration looks like this.

export default {
    build: {
        manifest: true,
        outDir: "www",
        emptyOutDir: false,
        rollupOptions: {
            input: '/src/scripts/main.js'
        }
    }
}

The only current drawback of Vite is that CSS files have to be included in JS (import "/src/styles/main.css") in order to build. As a result, they do get split out separately in the build, but some people may be bothered by this notation. CSS can also be linked directly via <link rel="stylesheet" href="src/styles/main.css">. If you do that, you still need to include them in JS to create a manifest entry. You can use a hack like this, for example:

	if (typeof window[9999] !== 'undefined') {
	  (async() => await import('/src/styles/main.css'))()
	}

Who doesn't like either solution, so there is no choice but to wait until the problem is solved in the this issue – https://github.com/…/issues/6595. Alternatively, one can use gulp-vite for the build, where the build can be handled via Gulp and Esbuild, and Vite can be used to allow loading things from sources, Web Sockets, etc.

We have used this approach in e.g. Newlogic Core, where Vite is used as a local server and everything else is handled by Gulp. The idea of this project was to have all modern development tools – like PostCSS, PurgeCSS, Tailwind, etc. – in one package. Example of integrating Newlogic Core with Nette on GitHub

If you use a combination of Vite, Stimulus and Naja with Nette you get a modern and easy to manage frontend. For a full demonstration of how to use Vite and modern frontend approaches, I also recommend checking out Newlogic UI.

If anyone is interested in a comparison of frontend tools over the last 10 years and how we developed Newlogic Core & UI, they can check out article.