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.
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
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.
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).
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.
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.
It's a little awkward, yes. But now you're set up with HMR through a Laravel Valet HTTPS served site.