idleberg / php-wordpress-vite-assets

Injects assets from a Vite manifest to the Wordpress head, supports themes and plugins
https://packagist.org/packages/idleberg/wordpress-vite-assets
MIT License
133 stars 12 forks source link

Feature Request: Inject Vite Client when in dev #15

Closed james0r closed 3 months ago

james0r commented 10 months ago

I've been using this script for a while to integrate my Vite assets in my WordPress theme, but noticed the other day that when I went to import CSS into my JS entryfile the PHP script failed to traverse my Vite manifest to find CSS chunks to inject.

So today I find php-wordpress-vite-assets which seems to do this just fine, but I'm noticing it doesn't handle the Vite client at all for running my Vite dev server.

I combined the two to achieve what I wanted, and seems to work fine but it would be nice if this package included Vite client injection so I could just depend on this package.

Here's what I got

Vite.lib.php

<?php

class Vite {

    /**
     * Flag to determine whether hot server is active.
     * Calculated when Vite::initialise() is called.
     *
     * @var bool
     */
    private static bool $isHot = false;

    /**
     * The URI to the hot server. Calculated when
     * Vite::initialise() is called.
     *
     * @var string
     */
    private static string $server;

    /**
     * The path where compiled assets will go.
     *
     * @var string
     */
    private static string $buildPath = 'build';

    /**
     * Manifest file contents. Initialised
     * when Vite::initialise() is called.
     *
     * @var array
     */
    private static array $manifest = [];

    public static function getHotState(): bool
    {
        return static::$isHot;
    }

    /**
     * To be run in the header.php file, will check for the presence of a hot file.
     *
     * @param  string|null  $buildPath
     * @param  bool  $output  Whether to output the Vite client.
     *
     * @return string|null
     * @throws Exception
     */
    public static function init(string $buildPath = null, bool $output = true): string|null
    {

        static::$isHot = file_exists(static::hotFilePath());

        // have we got a build path override?
        if ($buildPath) {
            static::$buildPath = $buildPath;
        }

        // are we running hot?
        if (static::$isHot) {
            static::$server = file_get_contents(static::hotFilePath());
            $client = static::$server . '/@vite/client';

            // if output
            if ($output) {
                printf(/** @lang text */ '<script type="module" src="%s"></script>', $client);
            }

            return $client;
        }

        // we must have a manifest file...
        if (!file_exists($manifestPath = static::buildPath() . '/manifest.json')) {
            throw new Exception('No Vite Manifest exists. Should hot server be running?');
        }

        // store our manifest contents.
        static::$manifest = json_decode(file_get_contents($manifestPath), true);

        return null;
    }

    /**
     * Enqueue the module
     *
     * @param string|null $buildPath
     *
     * @return void
     * @throws Exception
     */
    public static function enqueue_module(string $buildPath = null): void
    {
        // we only want to continue if we have a client.
        if (!$client = Vite::init($buildPath, false)) {
            return;
        }

        // enqueue our client script
        wp_enqueue_script('vite-client',$client,[],null);

        // update html script type to module wp hack
        Vite::script_type_module('vite-client');

    }

    /**
     * Return URI path to an asset.
     *
     * @param $asset
     *
     * @return string
     * @throws Exception
     */
    public static function asset($asset): string
    {
        if (static::$isHot) {
            return static::$server . '/' . ltrim($asset, '/');
        }

        error_log($asset);

        if (!array_key_exists($asset, static::$manifest)) {
            throw new Exception('Unknown Vite build asset: ' . $asset);
        }

        // look for chunked css
        if (array_key_exists('css', static::$manifest[$asset])) {
            return implode('/', [ get_stylesheet_directory_uri(), static::$buildPath, static::$manifest[$asset]['css'][0] ]);
        }

        return implode('/', [ get_stylesheet_directory_uri(), static::$buildPath, static::$manifest[$asset]['file'] ]);
    }

    /**
     * Internal method to determine hotFilePath.
     *
     * @return string
     */
    private static function hotFilePath(): string
    {
        return implode('/', [static::buildPath(), 'hot']);
    }

    /**
     * Internal method to determine buildPath.
     *
     * @return string
     */
    private static function buildPath(): string
    {
        return implode('/', [get_stylesheet_directory(), static::$buildPath]);
    }

    /**
     * Return URI path to an image.
     *
     * @param $img
     *
     * @return string|null
     * @throws Exception
     */
    public static function img($img): ?string
    {

        try {

            // set the asset path to the image.
            $asset = 'resources/img/' . ltrim($img, '/');

            // if we're not running hot, return the asset.
            return static::asset($asset);

        } catch (Exception $e) {

            // handle the exception here or log it if needed.
            // you can also return a default image or null in case of an error.
            return $e->getMessage(); // optionally, you can retrieve the error message

        }

    }

    /**
     * Update html script type to module wp hack.
     *
     * @param $scriptHandle bool|string
     * @return mixed
     */
    public static function script_type_module(bool|string $scriptHandle = false): string
    {

        // change the script type to module
        add_filter('script_loader_tag', function ($tag, $handle, $src) use ($scriptHandle) {

            if ($scriptHandle !== $handle) {
                return $tag;
            }

            // return the new script module type tag
            return '<script type="module" src="' . esc_url($src) . '" id="' . $handle . '-js"></script>';

        }, 10, 3);

        // return false
        return false;

    }

}

functions.php

use Idleberg\WordPress\ViteAssets\Assets;

 // enqueue the Vite module
    Vite::enqueue_module();

    if (Vite::getHotState()) {
      $cssUri = Vite::asset('src/theme.css');
      $jsUri = Vite::asset('src/theme.js');

      add_action('wp_enqueue_scripts', function () use ($jsUri, $cssUri) {
        wp_enqueue_style('theme-style', $cssUri, [], null, 'screen');
        wp_enqueue_script('theme-script', $jsUri, [], null, false);
      });

      Vite::script_type_module('theme-script');
    } else {
      $baseUrl = get_template_directory_uri() . '/build/';
      $manifest = "build/manifest.json";

      $jsEntry = "src/theme.js";
      $cssEntry = "src/theme.css";

      $viteAssets = new Assets($manifest, $baseUrl);
      $viteAssets->inject($jsEntry);
      $viteAssets->inject($cssEntry);
    }

Just wanted to hear thoughts on this.

Thanks!

DamChtlv commented 10 months ago

@james0r By curiosity, are you using a JS framework (ex: Vue) or not at all? And are you using vite v5 or v4? My incomprehension (#16) seems to be related with your FR

I tried using your code aswell but i have multiple concerns:

james0r commented 9 months ago

@DamChtlv No framework. I'm using Laravel Vite that creates the /hot file when in dev mode. Using Vite v4.5. Vite config is as follows

import { defineConfig } from 'vite'
import laravel from 'laravel-vite-plugin'
import fs from 'fs'
import { resolve } from 'path'

export default defineConfig({
  base: '',
  build: {
    emptyOutDir: true,
    manifest: true,
    outDir: 'build',
    assetsDir: 'assets'
  },
  server: {
    host: "0.0.0.0",
    https: false,
    hmr: {
      host: 'localhost',
    },
  },
  css: {
    postcss: {
      plugins: [
        require('postcss-import'),
        require('tailwindcss/nesting'),
        require('tailwindcss')(resolve(__dirname, './tailwind.config.js')),
        require('autoprefixer')
      ]
    }
  },
  plugins: [
    laravel({
      publicDirectory: 'build',
      input: [
        'src/theme.js',
        'src/theme.css',
        // 'src/scss/theme.scss'
      ],
      refresh: [
        '*/**/**.php'
      ]
    })
  ],
  resolve: {
    alias: [
      {
        find: /~(.+)/,
        replacement: process.cwd() + '/node_modules/$1'
      },
    ]
  }
})

Not sure what you mean about missing an init function. When in dev mode, scripts are injected here in Vite.lib.php

    public static function enqueue_module(string $buildPath = null): void
    {
        // we only want to continue if we have a client.
        if (!$client = Vite::init($buildPath, false)) {
            return;
        }

        if (!is_admin()) {
          // enqueue our client script
          wp_enqueue_script('vite-client',$client,[],null);
        }

        // update html script type to module wp hack
        Vite::script_type_module('vite-client');

    }

and when in production mode they're injected via idleberg's package here via the inject() method

         $viteAssets->inject($jsEntry, [
        'action' => 'wp_head',
        'integrity' => false
      ]);
      $viteAssets->inject($cssEntry, [
        'action' => 'wp_head',
        'integrity' => false
      ]);
DamChtlv commented 9 months ago

Thank you for taking time to answer!

I finally managed to make it work (after digging a lot of vite config, github repos, try & errors haha) I had few incomprehensions coming from webpack / gulp which is why i didn't understand how vite worked behind the scene. You're using laravel vite which also confused me 😅 I ended up creating my own integration in the end

Otherwise your proposal seems great :)

idleberg commented 9 months ago

What's the relationship between Laravel Vite and the script linked in the initial post? Does one depend on the other? Is it the same script or a stripped down version?

web1100manager commented 7 months ago

I have also been looking to get this to work with WordPress and Svelte and have run into similar issues of not having a manifest file in development mode.

github-actions[bot] commented 4 months ago

This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 14 days.

github-actions[bot] commented 3 months ago

This issue was closed because it has been stalled for 14 days with no activity.