vitejs / vite

Next generation frontend tooling. It's fast!
http://vitejs.dev
MIT License
67.22k stars 6.04k forks source link

Unwanted tailwindcss full refresh #9512

Open fabio-ivona opened 2 years ago

fabio-ivona commented 2 years ago

Describe the bug

After PR #3929 has ben released in V3,

In Laravel+tailwind+livewire stacks we are experiencing unwanted full refreshes triggered by changes on files registered as dependencies by tailwind

background

during Livewire components development, often with nested interactions, it is generally unwanted to full refresh the page until all template+css changes are completed, as it compels the developer to recreate again the same state of the component in order to show the part under development

Additionally a vite plugin has been released in order to trigger livewire components hot reloads when template files are changed (see here the plugin). The plugin uses

 ctx.server.ws.send({
        type: 'custom',
        event: 'livewire-update',
        data: {
            blade_updated: ctx.file.endsWith('.blade.php'),
        }
    });

to trigger a client-side script that starts the update routine for all livewire components in the page.

the problem

Both using and not using the vite livewire plugin, it is generally detrimental to have a full refresh when a .blade file changes

also, full page refreshes for .blade file changes is a feature already covered by laravel vite plugin (see here the plugin) with an opt-in feature in its config

the cause

PR #3929 fixes other issues by adding this code https://github.com/vitejs/vite/commit/d97b33a8cb9a72ed64244f239900a9a862b6ba68 to trigger a full refresh when a change happens in a .blade file registered as dependency by tailwind preprocessing

reproduction

the issue can be reproduced in any plain laravel+tailwind installation, seeing that the page is full refreshed at every .blade file change.

a ready to use repository is available at the reproduction link, it works with docker+docker-compose in order to setup an nginx+php+mysql environment, but can also be set up manually

see the repository readme for detailed steps

desired behaviour

in V2 the behaviour was the desired one:

when a .blade file was changes, tailwind recompiled app.css file, then Vite:

the solution

is there the possibility to make this part of code conditional? maybe with a vite config setting to disable it where needed?

I've set up a small Laravel/livewire project to reproduce the issue

Reproduction

https://github.com/fabio-ivona/livewire-refresh

System Info

tested both on

System:
    OS: Linux 5.13 Ubuntu 22.04.1 LTS 22.04.1 LTS (Jammy Jellyfish)
    CPU: (16) x64 AMD Ryzen 7 5700G with Radeon Graphics
    Memory: 17.04 GB / 31.14 GB
    Container: Yes
    Shell: 5.1.16 - /bin/bash
  Binaries:
    Node: 16.16.0 - /usr/bin/node
    Yarn: 1.22.19 - /usr/bin/yarn
    npm: 8.15.1 - /usr/bin/npm
  npmPackages:
    vite: ^3.0.0 => 3.0.4

and

System:
    OS: Linux 5.13 Ubuntu 21.10 21.10 (Impish Indri)
    CPU: (16) x64 AMD Ryzen 7 5700G with Radeon Graphics
    Memory: 17.10 GB / 31.14 GB
    Container: Yes
    Shell: 5.8 - /usr/bin/zsh
  Binaries:
    Node: 16.15.0 - /usr/local/bin/node
    npm: 8.5.5 - /usr/local/bin/npm
  Browsers:
    Chrome: 103.0.5060.134
    Firefox: 103.0.1
  npmPackages:
    vite: ^3.0.0 => 3.0.4

Used Package Manager

npm

Logs

Click to expand! ```shell vite:config bundled config file loaded in 55.85ms +0ms vite:config using resolved config: { vite:config plugins: [ vite:config 'vite:pre-alias', vite:config 'alias', vite:config 'vite:modulepreload-polyfill', vite:config 'vite:optimized-deps', vite:config 'vite:resolve', vite:config 'vite:html-inline-proxy', vite:config 'vite:css', vite:config 'vite:esbuild', vite:config 'vite:json', vite:config 'vite:wasm-helper', vite:config 'vite:worker', vite:config 'vite:asset', vite:config 'vite:wasm-fallback', vite:config 'vite:define', vite:config 'vite:css-post', vite:config 'vite:worker-import-meta-url', vite:config 'vite:dynamic-import-vars', vite:config 'vite:import-glob', vite:config 'laravel', vite:config 'vite:client-inject', vite:config 'vite:import-analysis' vite:config ], vite:config optimizeDeps: { vite:config disabled: 'build', vite:config force: undefined, vite:config esbuildOptions: { preserveSymlinks: undefined } vite:config }, vite:config server: { vite:config preTransformRequests: true, vite:config origin: '__laravel_vite_placeholder__', vite:config host: '0.0.0.0', vite:config port: 5173, vite:config strictPort: true, vite:config middlewareMode: false, vite:config fs: { strict: true, allow: [Array], deny: [Array] } vite:config }, vite:config base: '/', vite:config publicDir: '', vite:config build: { vite:config target: [ 'es2020', 'edge88', 'firefox78', 'chrome87', 'safari13' ], vite:config polyfillModulePreload: true, vite:config outDir: 'public/build', vite:config assetsDir: 'assets', vite:config assetsInlineLimit: 4096, vite:config cssCodeSplit: true, vite:config cssTarget: [ 'es2020', 'edge88', 'firefox78', 'chrome87', 'safari13' ], vite:config sourcemap: false, vite:config rollupOptions: { input: [Array] }, vite:config minify: 'esbuild', vite:config terserOptions: {}, vite:config write: true, vite:config emptyOutDir: null, vite:config manifest: true, vite:config lib: false, vite:config ssr: false, vite:config ssrManifest: false, vite:config reportCompressedSize: true, vite:config chunkSizeWarningLimit: 500, vite:config watch: null, vite:config commonjsOptions: { include: [Array], extensions: [Array] }, vite:config dynamicImportVarsOptions: { warnOnError: true, exclude: [Array] } vite:config }, vite:config resolve: { alias: [ [Object], [Object], [Object] ] }, vite:config ssr: { vite:config format: 'esm', vite:config target: 'node', vite:config noExternal: [ 'laravel-vite-plugin' ], vite:config optimizeDeps: { disabled: true, esbuildOptions: [Object] } vite:config }, vite:config configFile: '/var/www/html/vite.config.js', vite:config configFileDependencies: [ '/var/www/html/vite.config.js' ], vite:config inlineConfig: { vite:config root: undefined, vite:config base: undefined, vite:config mode: undefined, vite:config configFile: undefined, vite:config logLevel: undefined, vite:config clearScreen: undefined, vite:config optimizeDeps: { force: undefined }, vite:config server: {} vite:config }, vite:config root: '/var/www/html', vite:config cacheDir: '/var/www/html/node_modules/.vite', vite:config command: 'serve', vite:config mode: 'development', vite:config isWorker: false, vite:config mainConfig: null, vite:config isProduction: false, vite:config preview: { vite:config port: undefined, vite:config strictPort: true, vite:config host: '0.0.0.0', vite:config https: undefined, vite:config open: undefined, vite:config proxy: undefined, vite:config cors: undefined, vite:config headers: undefined vite:config }, vite:config env: { vite:config VITE_PUSHER_APP_KEY: '', vite:config VITE_PUSHER_HOST: '', vite:config VITE_PUSHER_PORT: '443', vite:config VITE_PUSHER_SCHEME: 'https', vite:config VITE_PUSHER_APP_CLUSTER: 'mt1', vite:config BASE_URL: '/', vite:config MODE: 'development', vite:config DEV: true, vite:config PROD: false vite:config }, vite:config assetsInclude: [Function: assetsInclude], vite:config logger: { vite:config hasWarned: false, vite:config info: [Function: info], vite:config warn: [Function: warn], vite:config warnOnce: [Function: warnOnce], vite:config error: [Function: error], vite:config clearScreen: [Function: clearScreen], vite:config hasErrorLogged: [Function: hasErrorLogged] vite:config }, vite:config packageCache: Map(0) {}, vite:config createResolver: [Function: createResolver], vite:config worker: { vite:config format: 'iife', vite:config plugins: [ vite:config [Object], [Object], [Object], vite:config [Object], [Object], [Object], vite:config [Object], [Object], [Object], vite:config [Object], [Object], [Object], vite:config [Object], [Object], [Object], vite:config [Object], [Object], [Object], vite:config [Object], [Object] vite:config ], vite:config rollupOptions: {} vite:config }, vite:config appType: 'spa', vite:config experimental: { importGlobRestoreExtension: false, hmrPartialAccept: false } vite:config } +57ms vite:deps Hash is consistent. Skipping. Use --force to override. +0ms VITE v3.0.4 ready in 255 ms ➜ Local: http://localhost:5173/ ➜ Network: http://172.27.0.7:5173/ LARAVEL v9.23.0 plugin v0.5.2 ➜ APP_URL: http://livewire-refresh.test vite:spa-fallback Rewriting GET / to /index.html +0ms vite:time 2.70ms /index.html +0ms vite:time 5.04ms /index.html +3ms vite:spa-fallback Rewriting GET / to /index.html +6ms vite:time 0.52ms /index.html +2ms vite:time 0.91ms /index.html +0ms vite:hmr [file change] storage/framework/sessions/5bVKUXpEXuUOZ0bLZZRyXLZ5WHgm4uDKplNJHS34 +0ms vite:hmr [no modules matched] storage/framework/sessions/5bVKUXpEXuUOZ0bLZZRyXLZ5WHgm4uDKplNJHS34 +1ms vite:resolve 1.90ms /@vite/client -> /var/www/html/node_modules/vite/dist/client/client.mjs +0ms vite:resolve 0.39ms /resources/css/app.css?direct -> /var/www/html/resources/css/app.css?direct +4ms vite:load 6.02ms [fs] /@vite/client +0ms vite:resolve 0.33ms @vite/env -> /var/www/html/node_modules/vite/dist/client/env.mjs +12ms vite:resolve 0.11ms /node_modules/vite/dist/client/env.mjs -> /var/www/html/node_modules/vite/dist/client/env.mjs +1ms vite:import-analysis 5.20ms [1 imports rewritten] node_modules/vite/dist/client/client.mjs +0ms vite:transform 9.23ms /@vite/client +0ms vite:time 21.74ms /@vite/client +248ms vite:load 14.64ms [fs] /resources/css/app.css?direct +13ms vite:cache [304] /@vite/client +0ms vite:time 0.37ms /@vite/client +4ms vite:load 4.74ms [fs] /node_modules/vite/dist/client/env.mjs +4ms vite:import-analysis 0.07ms [no imports] node_modules/vite/dist/client/env.mjs +7ms vite:transform 0.39ms /node_modules/vite/dist/client/env.mjs +7ms vite:time 1.37ms /node_modules/vite/dist/client/env.mjs +2ms vite:cache [304] /node_modules/vite/dist/client/env.mjs +332ms vite:time 0.44ms /node_modules/vite/dist/client/env.mjs +331ms vite:resolve 0.17ms /resources/views/welcome.blade.php -> /var/www/html/resources/views/welcome.blade.php +516ms vite:resolve 0.21ms /resources/views/livewire/hello-world.blade.php -> /var/www/html/resources/views/livewire/hello-world.blade.php +1ms vite:resolve 0.18ms /resources/js/app.js -> /var/www/html/resources/js/app.js +0ms vite:resolve 0.18ms /resources/js/bootstrap.js -> /var/www/html/resources/js/bootstrap.js +1ms vite:resolve 0.16ms /tailwind.config.js -> /var/www/html/tailwind.config.js +0ms vite:import-analysis [skipped] resources/css/app.css?direct +511ms vite:transform 515.90ms /resources/css/app.css?direct +511ms vite:time 532.72ms /resources/css/app.css +180ms vite:cache [304] /resources/css/app.css?direct +182ms vite:time 0.65ms /resources/css/app.css +2ms vite:deps ✨ static imports crawl ended +2s vite:hmr [file change] storage/framework/sessions/5bVKUXpEXuUOZ0bLZZRyXLZ5WHgm4uDKplNJHS34 +5s vite:hmr [no modules matched] storage/framework/sessions/5bVKUXpEXuUOZ0bLZZRyXLZ5WHgm4uDKplNJHS34 +0ms vite:hmr [file change] storage/framework/sessions/5bVKUXpEXuUOZ0bLZZRyXLZ5WHgm4uDKplNJHS34 +279ms vite:hmr [no modules matched] storage/framework/sessions/5bVKUXpEXuUOZ0bLZZRyXLZ5WHgm4uDKplNJHS34 +0ms vite:hmr [file change] storage/framework/sessions/5bVKUXpEXuUOZ0bLZZRyXLZ5WHgm4uDKplNJHS34 +339ms vite:hmr [no modules matched] storage/framework/sessions/5bVKUXpEXuUOZ0bLZZRyXLZ5WHgm4uDKplNJHS34 +0ms vite:hmr [file change] resources/views/livewire/hello-world.blade.php +11s 7:37:41 AM [vite] page reload resources/views/livewire/hello-world.blade.php vite:hmr [file change] storage/framework/views/4e0125ea13b45623cb2cef72879f41ca2753763e.php +50ms vite:hmr [no modules matched] storage/framework/views/4e0125ea13b45623cb2cef72879f41ca2753763e.php +0ms vite:hmr [file change] storage/framework/sessions/5bVKUXpEXuUOZ0bLZZRyXLZ5WHgm4uDKplNJHS34 +2ms vite:hmr [no modules matched] storage/framework/sessions/5bVKUXpEXuUOZ0bLZZRyXLZ5WHgm4uDKplNJHS34 +1ms vite:cache [304] /@vite/client +16s vite:time 1.45ms /@vite/client +16s vite:load 3.14ms [fs] /resources/css/app.css?direct +17s vite:import-analysis [skipped] resources/css/app.css?direct +16s vite:transform 28.68ms /resources/css/app.css?direct +16s vite:time 35.04ms /resources/css/app.css +36ms vite:cache [304] /@vite/client +38ms vite:time 1.25ms /@vite/client +2ms vite:cache [304] /resources/css/app.css?direct +1ms vite:time 0.53ms /resources/css/app.css +1ms vite:hmr [file change] .idea/workspace.xml +80ms vite:hmr [no modules matched] .idea/workspace.xml +0ms vite:cache [304] /node_modules/vite/dist/client/env.mjs +29ms vite:time 0.44ms /node_modules/vite/dist/client/env.mjs +29ms vite:cache [304] /node_modules/vite/dist/client/env.mjs +19ms vite:time 0.59ms /node_modules/vite/dist/client/env.mjs +19ms ```

Validations

sapphi-red commented 2 years ago

After PR https://github.com/vitejs/vite/pull/3929 has ben released in V3,

This was introduced in v2.4.0. So I guess this was happening before v3 or there is a different reason.

Both using and not using the vite livewire plugin, it is generally detrimental to have a full refresh when a .blade file changes

I suppose filtering the module in handleHotUpdate will prevent the refresh. https://github.com/def-studio/vite-livewire-plugin/blob/2023843edfe6ee1c2fc049407359ef916c93a4cd/src/index.ts#L239-L245

fabio-ivona commented 2 years ago

@sapphi-red missed that feature! thanks!

timacdonald commented 2 years ago

@sapphi-red this one is also impacting the official Laravel plugin.

It seems that Vite is triggering a complete browser refresh whenever files that Tailwind is configured to watch change, but I would have thought that Vite would be able to just push the new changes to the browser in a HMR way, just like it does when I edit an entry point.

Is this expected behaviour with Tailwind JIT? I can provide an example repo of the issue.

Feels like a workaround to have to introduce the handleHotUpdate function to the plugin just to filter this out.

sapphi-red commented 2 years ago

@timacdonald I think this is expected for now. When tailwind has foo.html in content option, it registers that as a dependency of bar.css (the content is @tailwind base; and others).

For this example, when foo.html is changed, Vite needs to trigger a full reload. Because Vite does not know whether only updating css by HMR is enough when foo.html is changed. (When a html is changed, the full reload is required because there is a dependency-relation which does not appear in the module graph. I think this should be enhanced but it's not a easy.)

That said, I agree the default behavior could be improved.

timacdonald commented 2 years ago

Thank you for the detailed explanation @sapphi-red I can see how that all comes together now.

MenMensAa commented 2 years ago

I changed the import method of my component from the relative import to the absolute import, and the problem was solved

fabio-ivona commented 2 years ago

@sapphi-red with your help I managed to stop the page full reload trigger bu returning [] after having handled the module myself:

        handleHotUpdate(ctx) {
          for (const pattern of pluginConfig.watch) {
                if ((0, minimatch_1.default)(ctx.file, pattern)) {
                   rivewireRefresh(ctx, pluginConfig);
                    return [];
                }
            }
        }

there is a problem with this solution, though: returning [] seems to prevent taiwlindcss from recompiling its css file

is there any way to trigger it in order to obtain a .css hot reload?

sapphi-red commented 2 years ago

@fabio-ivona I think you'll need to do this one

Filter and narrow down the affected module list so that the HMR is more accurate.

instead of returning [] (not tried though).

fabio-ivona commented 2 years ago

@sapphi-red the original Module list contains only the tailwind .css file

if I leave that one, the full reload occours, if I remove it, tailwind JIT doesn't recompile

so I cannot narrow it furthermore :disappointed:

a (really) dirty workaround is to add a dummy node ( in that module importers Set in order to make it fail the check in https://github.com/vitejs/vite/commit/d97b33a8cb9a72ed64244f239900a9a862b6ba68

if (ctx.modules[0]?.importers && ctx.modules[0].importers.size === 1) {
    const dummyModule = {...ctx.modules[0]};
    dummyModule.importers = new Set;
    dummyModule.isSelfAccepting = true;
    ctx.modules[0].importers.add(dummyModule);
}

It seems to work, but I really don't like my solution. Any idea?

sapphi-red commented 2 years ago

@fabio-ivona How about stopping at this condition? https://github.com/vitejs/vite/blob/b2c0ee04d4db4a0ef5a084c50f49782c5f88587c/packages/vite/src/node/server/hmr.ts#L246-L252

fabio-ivona commented 2 years ago

@fabio-ivona How about stopping at this condition?

@sapphi-red how can I obtain that?

sapphi-red commented 2 years ago

@fabio-ivona I think setting ctx.modules[0].importers = new Set() will make this for-loop skipped.

fabio-ivona commented 2 years ago

@sapphi-red wouldn't this skip tailwind jit compiling too?

fabio-ivona commented 2 years ago

@fabio-ivona I think setting ctx.modules[0].importers = new Set() will make this for-loop skipped.

@sapphi-red I confirm, it solves the full refresh issue, but it stops Tailwind jit compilation too (I guess because app.css is removed from node importers)

sapphi-red commented 2 years ago

@fabio-ivona Maybe it should be unwrapping instead.

        handleHotUpdate(ctx) {
          for (const pattern of pluginConfig.watch) {
                if ((0, minimatch_1.default)(ctx.file, pattern)) {
                   rivewireRefresh(ctx, pluginConfig);
                   return [...ctx.modules[0].importers, ...ctx.modules.slice(1)]
                }
            }
        }
fabio-ivona commented 2 years ago

@sapphi-red it works like a charm!

thanks for your hints and the awesome work you do here! :heart:

anhofmann commented 1 year ago

@fabio-ivona could you help me out to put the workaround in my laravel 9 + tailwind + livewire project? I tried to understand the vite Plugin API, but I'm unable to put the handleHotUpdate code in the right place.

fabio-ivona commented 1 year ago

Hi @anhofmann , are you using my Vite Livewire plugin? Or is it a custom code?

anhofmann commented 1 year ago

Thank you @fabio-ivona for the fast reply! To be honest, I have no idea what I'm using. I've created a Laravel 9 project two months ago with Jetstream and I'm using Livewire. As far as I understand, my setup is the default that comes with Laravel. And I have the full page refresh effect, that is described in this issue.

My vite.config.js looks like this:

import { defineConfig } from 'vite';
import laravel, { refreshPaths } from 'laravel-vite-plugin';

export default defineConfig({
    plugins: [
        laravel({
            input: [
                'resources/css/app.css',
                'resources/js/app.js',
            ],
            refresh: [
                ...refreshPaths,
                'app/Http/Livewire/**',
                'app/Forms/Components/**',
            ],
        }),
    ],
});

BTW: I tried to set refresh to false, but when I run sail npm run dev, I still have the full page reloads on file changes. No idea why my setting gets ignored. I was hoping to be able to disable the reload mechanism, or have a reload mechanism, that doesn't reload the full page, because this destroys the state in my Livewire components.

This is my postcss.config.js

module.exports = {
    plugins: {
        tailwindcss: {},
        autoprefixer: {},
    },
};

and this my package.json

{
    "private": true,
    "scripts": {
        "dev": "vite",
        "build": "vite build"
    },
    "devDependencies": {
        "@alpinejs/focus": "^3.10.5",
        "@tailwindcss/forms": "^0.5.2",
        "@tailwindcss/typography": "^0.5.4",
        "alpinejs": "^3.10.3",
        "autoprefixer": "^10.4.7",
        "axios": "^1.1.2",
        "laravel-vite-plugin": "^0.7.2",
        "postcss": "^8.1.14",
        "tailwindcss": "^3.1",
        "vite": "^4.0.0"
    }
}
fabio-ivona commented 1 year ago

@anhofmann you can use this to solve the issue, both by directly using the plugin or by implementing your custom solution and taking my code as an example:

https://github.com/defstudio/vite-livewire-plugin