January 28th, 2022 by Alex Garrett-Smith

Laravel Mix HMR with HTTPS

I use Laravel Valet for day-to-day development, and I recently got stuck getting Laravel Mix HMR (Hot Module Replacement) to work while I had HTTPS enabled through the 'valet secure' command.

If that's you too, here's a guide on how to get it working flawlessly.

Fresh project

I'm writing this article alongside a freshly installed Laravel project. It's a good idea to start with a fresh app for this kind of thing, just to avoid anything else getting in the way.

Once you've got everything working, you can transfer everything to your existing site and investigate any issues.

Create a fresh Laravel project.

laravel new mixhmrhttps

Because I'm using Laravel Valet, I should immediately be able to access http://mixhmrhttps.test in the browser.

Make sure HTTPS is enabled for this project.

valet secure mixhmrhttps

And now we can access https://mixhmrhttps.test. Great.

Now install any npm dependancies and start HMR.

npm i && npm run hot

Broken assets

Everything at this point will look fine under HTTPS. Let's replace the default welcome.blade.php file with a basic document that pulls in our compiled app.js and app.css files.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
 
<script src="{{ mix('/js/app.js') }}" defer></script>
<link href="{{ mix('/css/app.css') }}" rel="stylesheet" />
</head>
<body>
My app
</body>
</html>

Now if you head over to your site, you'll notice the assets are not being loaded. Taking a closer look, our assets are being loaded from localhost:8080 and we have some HTTPS errors.

This is what we need to fix, so let's head over and configure this to work.

Configuring webpack.mix.js

The first thing to change is the HMR host. Right now we're making requests to localhost:8080.

Add an options call to your mix file to specify the HMR options. You'll end up with something that looks like this.

mix.js('resources/js/app.js', 'public/js')
.postCss('resources/css/app.css', 'public/css', [
//
])
.options({
hmrOptions: {
host: 'mixhmrhttps.test',
port: 8080
}
})

That fixes the hostname, but not the HTTPS issue.

To fix this, we'll specify the paths to our key and certificate that Laravel Valet generated for us when we previously ran the valet secure command.

The webpackConfig method on Mix allows us to specify additional Webpack config for the dev server.

const fs = require('fs')
 
//...
 
mix.js('resources/js/app.js', 'public/js')
.postCss('resources/css/app.css', 'public/css', [
//
])
.options({
hmrOptions: {
host: 'mixhmrhttps.test',
port: 8080
}
})
.webpackConfig({
devServer: {
https: {
key: fs.readFileSync('/Users/alexgarrettsmith/.config/valet/Certificates/mixhmrhttps.test.key'),
cert: fs.readFileSync('/Users/alexgarrettsmith/.config/valet/Certificates/mixhmrhttps.test.crt')
}
}
})

Don't forget to require fs somewhere at the top of your mix file!

If this looks a little scary, don't worry. Just replace my details with your own username/site name to match up to the key and certificate. **Really importantly **we'll tidy this up later to avoid issues in production, so read on!

Ok, with that config added. Let's check it out.

Nice! We're successfully loading in those two assets (and any more you add in future).

What about production?

When you deploy this app (and run npm run prod), these configuration options will be included, which may cause issues.

To avoid this, we'll make use of Laravel Mix's built-in production check.

Change your mix.config.js file to the following.

mix.js('resources/js/app.js', 'public/js')
.postCss('resources/css/app.css', 'public/css', [
//
])
 
if (mix.inProduction()) {
mix.options({
hmrOptions: {
host: 'mixhmrhttps.test',
port: 8080
}
})
.webpackConfig({
devServer: {
https: {
key: fs.readFileSync('/Users/alexgarrettsmith/.config/valet/Certificates/mixhmrhttps.test.key'),
cert: fs.readFileSync('/Users/alexgarrettsmith/.config/valet/Certificates/mixhmrhttps.test.crt')
}
}
})
}

Now our hmrOptions and devServer stuff will only be used for the dev and hot commands.

If you need to specify Laravel Mix options or any additional Webpack config in production, your mix.config.js file will probably end up looking something like this.

mix.js('resources/js/app.js', 'public/js')
.postCss('resources/css/app.css', 'public/css', [
//
])
 
if (!mix.inProduction()) {
mix.webpackConfig({
//...
})
}
 
if (mix.inProduction()) {
// the stuff we've covered already
}

It's really up to you. Just make sure you differentiate between your development and production environments. You could even create a separate mix.config.js file for each environment. For something this simple, a couple of IF statements do the trick.

Bonus! Hiding your key and certificate paths.

Your mix.config.js file will always be pushed to source control, so it makes sense to control the path through your .env file (which should not be pushed to production). This also makes it really easy for another developer to specify their own paths when working on their machine!

Start by requiring in dotenv at the top of your mix file.

require('dotenv').config()
 
//...

Now add the environment variables to your .env file.

MIX_HTTPS_CERT="/Users/alexgarrettsmith/.config/valet/Certificates/mixhmrhttps.test.crt"
MIX_HTTPS_KEY="/Users/alexgarrettsmith/.config/valet/Certificates/mixhmrhttps.test.key"

And finally, swap over the direct reference to the key and certificate.

.webpackConfig({
devServer: {
https: {
key: fs.readFileSync(process.env.MIX_HTTPS_KEY),
cert: fs.readFileSync(process.env.MIX_HTTPS_CERT)
}
}
})

Much cleaner, and easier to change this between different local environments.

Conclusion

It's a little awkward, yes. But now you're set up with HMR through a Laravel Valet HTTPS served site.