Skip to Main Content

Hot Module Reloading with Laravel Mix v6 and Craft Nitro 2

Posted on under Craft Development / Front End

Just show me the config!
Okay here... (but read this first)

I have also forked Craft's starter-blog template with a working Mix 6/Twigpack setup here.

Not using Mix but another wrapper like vue-cli-service or just straight webpack? Check this out!

The Challenge

Hot module reloading is kind of a funny thing. On one hand, it feels like the gold standard, must-have tool for local development. On the other, it's fairly complicated to setup by hand and only works automagically in certain situations. Some even say it's overrated, but I'd argue that if your local development stack is hitting a server, even a local one, doing a hard refresh just to see a style change can really slow things down.

In the case of Nitro 2, the default assumptions that webpack-dev-server makes about how to provide hot module reloading (e.g. running the server on localhost:8080 does not apply.

Much like Laravel Mix is a user-friendly wrapper for webpack, Nitro 2 is a user-friendly wrapper for Docker. It gives us an easily modifiable yet disposable development environment that closely simulates that of a production server. That said, Docker communicates with your host machine in very specific ways. The localhost:8080 you may have running on your host machine is not necessarily accessible from within the Docker container (your Nitro 2 site) and vise-versa.

Resources

Assumptions Made

There are a lot moving parts required to get this working, including:

  • Nitro v2.0.7+ (run nitro version to confirm that the Nitro CLI and Nitro gRPC are both the same version. If not, try running nitro self-update and nitro update then nitro apply)
  • Laravel Mix v6.0+ (or just webpack 5.0+)
  • Twigpack plugin

Paradigm Shift

You have two options when it comes to running npm commands while using Nitro: run it on your host machine (i.e. as you normally would) or run it within the Docker container.

In our case, we are going to run our npm command from within the Docker container. With Nitro you do this by first SSHing into your project via nitro ssh <project-name> or if you're already in your project directory, simply nitro ssh. Once inside you can run NPM commands like you normal would, e.g. npm install && npm run hot.

Note: Nitro does not ship with yarn, so if you or your team prefers yarn, you can first install your packages via yarn from your host machine like normal then SSH into your project and use npm to run the development script.

Stumbling Blocks

Nitro NPM Command

One of the commands Nitro ships with is nitro npm, allowing you to run npm commands from within the nitro container as mentioned above, e.g. nitro npm install. But for whatever reason, nitro npm run hot does not yield the same results as doing nitro ssh then npm run hot.

Use nitro ssh then npm run hot NOT nitro npm run hot. (substitute hot for whatever your development script name is).

Exposed Ports

webpack-dev-server relies on a specific port to run and communicate with your site. Since the dev-server will be running within a Docker container, we will need to assign it to a port that Docker is exposing to our host machine. In this case we have two options: 3000 or 3001. Don't try to use anything else - it won't work.

Laravel Mix 6 Sample Configuration

Just a quick reminder that Mix 6 uses webpack 5 and, more importantly, webpack-dev-server 4.0 beta under the hood. The dev-server beta has a lot of differences and breaking changes that we need to be mindful of as we proceed.

Note that I have extracted all of my variable settings into a config object to keep the devServer config nice and clean.

  
    let mix = require('laravel-mix');

const config = {
  host: process.env.HMR_SHARED_HOST,
  port: process.env.HMR_PORT,
  path: process.env.HMR_PATH,
  clientHost: process.env.HMR_SITE_HOST,
  https: process.env.HMR_HTTPS === "true",
  protocol: process.env.HMR_HTTPS === "true" ? 'https://' : 'http://',
  outputPath: function(){
    return `${this.protocol}${this.clientHost}:${this.port}${this.path}`
  },
}

// ? ========== DEVELOPMENT SETTINGS ==========
if(!mix.inProduction()){
  mix.setPublicPath('public/build/')
     .js('./src/js/app.js', 'js')
     .css('./src/css/app.css', 'css')
  mix.webpackConfig({
      target: 'web',
      output: {
        publicPath: config.outputPath()
      },
      devServer:{
        host: config.host,
        port: config.port,
        https: config.https,
        dev: {
          publicPath: config.path,
        },
        client: {
          port: config.port,
          host: config.clientHost,
        },
        firewall: false,
        static: {
          directory: './templates',
          publicPath: '/',
          watch: true
        },
        liveReload: true,
      },
      infrastructureLogging: {
        level: 'log',
      },
    });
}  
  
    HMR_SITE_HOST="andrewmenich.test"
HMR_SHARED_HOST="0.0.0.0"
HMR_PORT=3000
HMR_PATH="/build/"
HMR_HTTPS=false  
  
    'devServer' => [
  'manifestPath' => 'http://localhost:8080/build/',
  'publicPath' => 'http://andrewmenich.test:3000/build/',
],  

.env Breakdown

HMR_PORT - As mentioned above, Nitro exposes ports 3000 and 3001, so either of those will work.

HMR_SHARED_HOST - This is what is used for the normal host setting. The '0.0.0.0' value makes it accessible externally.

HMR_SITE_HOST - This is the same name as the site you're developing on and used in the Client and Output configs. You may already have the value you need set to another variable, like DEFAULT_SITE_URL so feel free to use that instead of creating another variable. In theory, we should be able to use '0.0.0.0' here as well, but I got nothing but CORS errors and adding the appropriate headers to the config didn't seem to help.

HMR_PATH - The directory you want your dev-server accessible from. This should match whatever you're setting in mix.setPublicPath().

HMR_HTTPS - Nitro allows you to serve sites over https, so if you prefer that, set this to "true" in the env so the dev-server matches. (Full disclosure: I didn't test this so your mileage may vary.)

Mix Breakdown

  
    devServer:{
  host: config.host,
  port: config.port,
  dev: {
    publicPath: config.path,
  },
  firewall: false,
}  

The host, port, and publicPath settings define the address our dev-server files will be accessed at. In our case the mix-manifest.json file will be accessed at 0.0.0.0:3000/build/mix-manifest.json. firewall is the new setting that replaces disableHostCheck.

  
    devServer: {
  client: {
    port: config.port,
    host: config.clientHost,
  },
}  


The client configuration adjusts the host and port of the websocket that the dev-server creates. This is where a lot of the HMR magic happens. Notice that we set the host to our local development URL rather than '0.0.0.0'.

  
    devServer: {
  static: {
    directory: './templates',
    publicPath: '/',
    watch: true
  },
  liveReload: true,
}
infrastructureLogging: {
  level: 'log',
},  

Here we're telling webpack about our static files (twig templates) so that it will automatically trigger a hard refresh whenever we make changes to those templates. Lastly, I always think more information is better when developing so we can increase the logging output by setting infrastructueLogging setting.

  
    'devServer' => [
  'manifestPath' => 'http://localhost:8080/build/',
  'publicPath' => 'http://andrewmenich.test:3000/build/',
],  

The last piece of the puzzle is telling Twigpack where to find and serve the local dev files. It's important to remember that Twigpack is running from within the Docker container and not your host machine, so the URL it accesses the development files at is different than the URL you access the development files at, hence the discrepancy.

If you change the path from the example provided here and you need to debug, SSH into your Nitro site and use curl to ping one of your files. e.g. curl "http://localhost:8080/build/mix-manifest.json". While inside your site's container, dev-server files will be accessible via port 8080 (localhost or 0.0.0.0) and not 3000 or 3001.

webpack-only tip

If you're not using Mix, you might need to change your manifestPath port to 3000. Mix always writes the manifest file to the disk, but some webpack configurations do not. In this case, it will be available in-memory, i.e. from port 3000 where your dev-server is running.

🚀🚀🚀

You should now have hot module reloading for your local Nitro 2 development!

Not Using Mix 6?

webpack 5

If you're just using webpack 5, the above configuration should also work for you, assuming that you also have the webpack-dev-server 4.0beta+ installed.

webpack 4

If you're using Mix 5 or webpack 4 (and by default, webpack-dev-server 3) then the configuration looks slightly different. (In this example my local development site is 'mix5.test'.

  
    output: {
  publicPath: "http://mix5.test:3000/build/",
},
devServer:{
  host: '0.0.0.0',
  port: 3000,
  sockHost: 'mix5.test',
  sockPort: 3000,
  publicPath: '/build/',
  headers: {
    "Access-Control-Allow-Origin": '*',
    "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE",
  },
  disableHostCheck: true,
},  

Other Configs

On occasion you might be hard-coding a webpack global variable in your js entry file. If you're doing this for the publicPath e.g. __webpack_public_path__ = make sure to set this to your local dev URL with the appropriate port and path to match your devServer config.

  
    __webpack_public_path__ = 'http://mynitrosite.test:3000/build/'  

Still Stuck?

If you're still stuck, here are some tips/reminders that might get you on the right track.

  • The port value should only ever be 3000 or 3001 with one exception being the manifestPath value in your Twigpack config, which uses port 8080.
  • Make sure your publicPath value matches the value you define in your Mix config.
  • If your CSS/JS files load properly from the manifest file but webpack-dev-server instantly shuts down and throws errors, adjust your client settings in Mix 6 / webpack 5 or your sockPort & sockHost settings in Mix 5 / webpack 4.
  • If your hot files are not being inject after making a change to the CSS or JS, adjust your output > publicPath settings.