Utiliser ViteJS avec Symfony

Voir la vidéo

Dans cette vidéo je vous propose de découvrir comment intégrer ViteJS dans un projet Symfony.

Pourquoi ?

Avant même d'aller plus loin on peut se demander l'intérêt d'utiliser ViteJS sur Symfony, surtout qu'il propose déjà une gestion des assets via Symfony Encore.
ViteJS possède plusieurs avantages :

  • Un serveur de développement plus rapide que celui proposé par Webpack avec le support du hot reload et du fast refresh dans le cas de React.
  • La configuration est plus simple pour les besoins les plus répandus et vous aurez la main sur la configuration si vous avez des besoins plus spécifiques (on créera par exemple un plugin pour actualiser la page au changement de fichiers Twig).

Aussi, cet exercice vous permettra de découvrir le fonctionnement de l'import des assets et de l'adapter à d'autres outils.

Comment ?

Nous allons créer une fonction Twig qui va permettre de charger un asset particulier en adaptant le chemin en fonction de l'environnement.

  • En développement, les assets seront récupérées depuis le serveur de développement de Vite : http://localhost:3000/assets/main.jsx.
  • En production, les assets auront un hash (pour l'invalidation du cache) et devront être récupérées depuis le dossier public : /assets/main.jsx.13NJ13U04N.js

C'est ce second cas qui est souvent problématique car il faut être capable de récupérer les noms des fichiers compilés. Heureusement ViteJS permet la génération d'un fichier manifest.json qui contiendra la liste de nos différents points d'entrés et les fichiers générés.

{
  "main.jsx": {
    "file": "main.jsx.0312cc1b.js",
    "src": "main.jsx",
    "isEntry": true,
    "dynamicImports": [
      "demo.js"
    ],
    "css": [
      "main.jsx.35ea8056.css"
    ],
    "assets": [
      "logo.ecc203fb.svg"
    ]
  },
  "demo.js": {
    "file": "demo.441114fc.js",
    "src": "demo.js",
    "isDynamicEntry": true
  }
}

Il faudra donc lire et parser ce fichier pour obtenir le chemin vers les fichiers CSS / JS (et utiliser du cache pour éviter de répéter cette étape à chaque chargement).

Un peu de code

En premier lieu il faudra adapter la configuration de ViteJS à l'architecture de Symfony

import { defineConfig } from 'vite';
import reactRefresh from '@vitejs/plugin-react-refresh'; // Spécifique à react
import { resolve } from 'path';

// (optionel) Ce plugin permet de lancer un refresh de la page lors de la modification d'un fichier twig
const twigRefreshPlugin = {
  name: 'twig-refresh',
  configureServer ({ watcher, ws }) {
    watcher.add(resolve('templates/**/*.twig'));
    watcher.on('change', function (path) {
      if (path.endsWith('.twig')) {
        ws.send({
          type: 'full-reload'
        });
      }
    });
  }
}

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [reactRefresh(), twigRefreshPlugin],
  root: './assets',
  base: '/assets/',
  server: {
    watch: {
      disableGlobbing: false // nécessaire pour le plugin twig
    }
  },
  build: {
    manifest: true,
    assetsDir: '',
    outDir: '../public/assets/',
    rollupOptions: {
      output: {
        manualChunks: undefined // On ne veut pas créer un fichier vendors, car on n'a ici qu'un point d'entré
      },
      input: {
        'main.jsx': './assets/main.jsx'
      }
    }
  }
});

Ensuite on va enregistrer la fonction à utiliser dans Twig :

<?php

namespace App\Twig;

use Psr\Cache\CacheItemPoolInterface;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;

class ViteAssetExtension extends AbstractExtension
{

    private ?array $manifestData = null;
    const CACHE_KEY = 'vite_manifest';

    public function __construct(
        private bool $isDev,
        private string $manifest,
        private CacheItemPoolInterface $cache,
    ) {}

    public function getFunctions(): array
    {
        return [
            new TwigFunction('vite_asset', [$this, 'asset'], ['is_safe' => ['html']])
        ];
    }

    public function asset(string $entry, array $deps)
    {
        if ($this->isDev) {
            return $this->assetDev($entry, $deps);
        }
        return $this->assetProd($entry);
    }

    public function assetDev(string $entry, array $deps): string
    {
        $html = <<<HTML
<script type="module" src="http://localhost:3000/assets/@vite/client"></script>
HTML;
        if (in_array('react', $deps)) {
            $html .= '<script type="module">
                import RefreshRuntime from "http://localhost:3000/assets/@react-refresh"
    RefreshRuntime.injectIntoGlobalHook(window)
    window.$RefreshReg$ = () => {}
    window.$RefreshSig$ = () => (type) => type
    window.__vite_plugin_react_preamble_installed__ = true
        </script>';
        }
        $html .= <<<HTML
<script type="module" src="http://localhost:3000/assets/{$entry}" defer></script>
HTML;
        return $html;
    }

    public function assetProd(string $entry): string
    {
        if ($this->manifestData === null) {
            $item = $this->cache->getItem(self::CACHE_KEY);
            if ($item->isHit()) {
                $this->manifestData = $item->get();
            } else {
                $this->manifestData = json_decode(file_get_contents($this->manifest), true);
                $item->set($this->manifestData);
                $this->cache->save($item);
            }
        }
        $file = $this->manifestData[$entry]['file'];
        $css = $this->manifestData[$entry]['css'] ?? [];
        $imports = $this->manifestData[$entry]['imports'] ?? [];
        $html = <<<HTML
<script type="module" src="/assets/{$file}" defer></script>
HTML;
        foreach($css as $cssFile) {
            $html .= <<<HTML
<link rel="stylesheet" media="screen" href="/assets/{$cssFile}"/>
HTML;
        }

        foreach($imports as $import) {
            $html .= <<<HTML
<link rel="modulepreload" href="/assets/{$import}"/>
HTML;
        }

        return $html;
    }

}

Enfin, il faudra enregistrer le service afin de lui passer les arguments nécessaires.

services:

    # ...

    App\Twig\ViteAssetExtension:
        arguments:
            $isDev: '%env(VITE_DEV)%'
            $manifest: '%kernel.project_dir%/public/assets/manifest.json'
            $cache: '@vite_cache_pool'

On utilisera ici une variable d'environnement pour activer l'utilisation du serveur de développement.

Publié
Technologies utilisées
Auteur :
Grafikart
Partager