You Might Not Need Laravel Mix

Published

Laravel Mix is incredibly helpful. As far as I am aware it is the easiest way to get up and running with webpack when you are working on a backend application. However, there are certain situations where even Laravel Mix might be more than you need. This is particularly true when working with a TALL stack application where most of your styling will be generated by Tailwind CSS and most of your Javascript will be managed by Alpine or Livewire.

Let's take a look at an alternative asset pipeline configuration that has much less overhead and works particularly well in the TALL stack context.

TLDR? See the final package.json definition here.

Javascript

esbuild is a javascript bundler written in golang. It has a very small footprint, and it compiles javascript very quickly. It can be installed with NPM:

npm install esbuild --save-dev 

To compile javascript we pass it our main javascript file using flags to indicate the type of output we want to create. For example, to build our javascript for local development we might do something like this:

./node_modules/.bin/esbuild resources/js/app.js --outfile=public/js/app.js --target=es6 --bundle --sourcemap

A production build might look something like this:

./node_modules/.bin/esbuild resources/js/app.js --outfile=public/js/app.js --target=es6 --bundle --minify --define:process.env.NODE_ENV=\\\"production\\\"

The first argument is our javascript entry point; the file that serves as the orchestrator of our javascript bundle. In that file you will import any third party packages you are using, as well as any custom javascript you have written for your application. In most Laravel applications this is the resources/js/app.js file, but it may be different in your project.

The --outfile argument tells esbuild where you want the compiled javascript to be written. In this case we are sending it to public/js/app.js. As a side note, I recommend adding automatically generated files like this to .gitignore so that they are not tracked in your repository. This will will reduce commit noise over the long run, though it does mean that you will need to run the asset pipeline on each deployment - this is something you will likely be doing anyway.

The --target argument tells esbuild what format of javascript you want to compile to. This can be either a language version such as "es6" or "es2020", or a list of browser engines: "chrome", "firefox", "safari", etc. The default value is "esnext", which is very cutting edge. At the moment I am using the "es6" target for most of my projects, but that will likely change in the future.

The --bundle flag tells esbuild to bundle all of the javascript together, rather than linking out to separate files in the node_modules folder. I consider this to be optional for local development, but it is a requirement for production builds; without it your javascript will break in production, or behave unexpectedly at the very least.

The --sourcemap flag will trigger the creation of a source map file which can be very helpful when doing local development. The minify flag triggers automatic code minimization, as you can imagine.

The --define argument has many uses; here we are telling esbuild to operate with the NODE_ENV=production environment variable in place. Note the extra escape slashes - these are required to ensure that the value is parsed correctly by the binary.

Styles

PostCSS is "a tool for transforming CSS with javascript". It is incredibly versatile, and it is the engine that powers TailwindCSS. You will find that PostCSS is the best way to start working with next generation CSS code in your projects today. Most of the features you might find useful in SASS or LESS can be replicated with PostCSS fairly easily; it is possible that you could remove those tools entirely from your project and just use PostCSS instead.

We will use the PostCSS-CLI tool to run our css files through postcss and generate our compiled styles file. We can install both PostCSS and the CLI tool like so:

npm install postcss postcss-cli --save-dev

Before we can run the CLI build tool we have to define a configuration file that will tell PostCSS how to operate.

1module.exports = {
2 plugins: {
3 tailwindcss: {},
4 autoprefixer: {},
5 }
6}
postcss.config.js

Here we are registering two plugins: Tailwind and Autoprefixer. These will need to be installed via NPM before you can use them in your asset pipeline. There are many plugins available in the PostCSS ecosystem; the possibilities are endless.

To create a css file for local development we can run this command:

./node_modules/.bin/postcss resources/css/app.css --output public/css/app.css --map --verbose

As with esbuild, the first argument we pass to the binary is the file we want to have processed. This is the entry point into our CSS. You could also point it to a directory if you want to combine multiple files together.

The --output argument names the destination for our compiled CSS. The --map flag tells PostCSS to generate a source map. The --verbose option tells the CLI to print contextual information to the terminal; without it the command output is very minimal.

To compress our styles in production we will use cssnano, which is a PostCSS plugin for minimizing CSS. We will only want to run minimization for production builds; to do that we will need to modify our postcss.config.js file to provide it with some context:

1module.exports = (ctx) => ({
2 plugins: {
3 tailwindcss: {},
4 autoprefixer: {},
5 cssnano: ctx.env === 'production' ? {} : false,
6 },
7})
postcss.config.js

This tells PostCSS to only use the cssnano plugin when the NODE_ENV environment variable is set to 'production'. To generate our production build we can modify our build command like so:

NODE_ENV=production ./node_modules/.bin/postcss resources/css/app.css --output public/css/app.css --verbose

The CLI tool has many more options that are worth investigating.

Watching Files

Our local development experience can improve quite a bit if we can have our asset pipeline monitor our resource files and re-compile them automatically when they are changed. Fortunately, both esbuild and the postcss-cli provide options for watching files and compiling them automatically for us.

For esbuild we can add a --watch flag to monitor our javascript files:

./node_modules/.bin/esbuild resources/js/app.js --outfile=public/js/app.js --target=es6 --bundle --sourcemap --watch

The PostCSS-CLI tool has the same option:

./node_modules/.bin/postcss resources/css/app.css --output public/css/app.css --map --verbose --watch

If we define a separate NPM script for each of these commands we can then run them both at the same time:

npm run watch:js & npm run watch:css

With that in place we should have everything we need to work on our application locally and then deploy it to production.

Our new Package.json file

Here is a package.json file that includes all of the dependencies we have discussed and the scripts we have defined:

1{
2 "private": true,
3 "scripts": {
4 "build:css": "NODE_ENV=production postcss resources/css/app.css --output public/css/app.css --verbose",
5 "build:js": "esbuild resources/js/app.js --outfile=public/js/app.js --target=es6 --bundle --minify --define:process.env.NODE_ENV=\\\"production\\\"",
6 "build": "npm run build:js && npm run build:css",
7 "local:css": "postcss resources/css/app.css --output public/css/app.css --map --verbose",
8 "local:js": "esbuild resources/js/app.js --outfile=public/js/app.js --target=es6 --bundle --sourcemap",
9 "local": "npm run local:js && npm run local:css",
10 "watch:css": "postcss resources/css/app.css --output public/css/app.css --map --verbose --watch",
11 "watch:js": "esbuild resources/js/app.js --outfile=public/js/app.js --target=es6 --bundle --sourcemap --watch",
12 "watch": "npm run watch:js & npm run watch:css"
13 },
14 "dependencies": {
15 "alpinejs": "^2.8.1"
16 },
17 "devDependencies": {
18 "autoprefixer": "^10.2.5",
19 "esbuild": "^0.9.4",
20 "postcss": "^8.2.8",
21 "postcss-cli": "^8.3.1",
22 "tailwindcss": "^2.0.4"
23 }
24}
package.json

Notes:

  • I recommend that you add all of your generated assets to the .gitignore file and then rebuild them as part of your deployment process.
  • In the build and local scripts above we separate our commands with two ampersands; this means that if the first command fails the second one will not run.
  • In the watch script above we separate our commands with one ampersand; this means they will be run simultaneously.
  • When you remove Laravel Mix from your project, you can no longer use the mix() helper in your template files.
  • I have not tried running these commands on Windows - your mileage may vary.

Asset Versioning

The final step in setting up our asset pipeline will be implementing asset versioning for browser cache busting. This, however, will be a topic for a separate post.

Tools Referenced In This Post

About the Author

Ryan C. Durham is a software developer who lives in southern Washington with his wife and daughter. His areas of interest include PHP, Laravel, Rust and PostgreSQL, as well as organizational efficiency and communications strategies.

You can find him on GitHub and LinkedIn.