Setting up and Using Grunt with Less in Magento 2

If you made it through the first post in this series you might have noticed two things. One, it was a very long post. Two, the process of getting a result on the page was absurd. The goal of that post was not to show you the best practice in front end development but all the moving parts involved in Magento’s Less system.

Today we change that and set up a task running to make the front end workflow useable and modern.

What is Grunt?

Grunt is a task runner that will execute scripts that do work for you. In the previous post, we used Magento’s built in bin/magento setup:static-content:deploy -f to compile our Less code into CSS. If we were to do that every time we make a change to our Less files, we would loose our minds. That system was not designed for theme development. For our day to day work, we use a task running to watch our theme’s Less files and compile them as we make changes. That is Grunt’s job.

Why not gulp?

Soon after the release of Magento 2.0 there was a heated discussion on why Grunt was chosen over Gulp, which at that time has overshadowed Grunt in speed and adoption. To be honest, i don’t know why Grunt was chosen. Both work well for the tasks they are running. I have used both and will be writing a post for setting up Gulp similar to this one. If you are accustomed to Gulp and don’t want to bother with Grunt, you can get started here: https://devdocs.magento.com/guides/v2.3/frontend-dev-guide/css-topics/gulp-sass.html

Getting started

Grunt is a Javascript task running. So that means it needs to both have NPM running and it needs to be installed on your Magento instance (all the configuration file are there when you install Magento, but we have to setup Grunt ourselves). We will be using Mark Shust’s docker project for setting up and using Magento and Grunt. But the instructions here will be the same for any local install. The only difference is you will be omitting the src in your directory path and you will use the NPM command directly, as you will not be executing these inside a Docker container. The benefit of using a Docker container is that you wont have to install NPM locally or bother with versions and compatibilities.

Inside the root of your Magento install, run this command: ls bin. You will get a output like this:

bash				dev-urn-catalog-generate	magento				remove				setup-pwa-studio
cli				devconsole			n98-magerun2			removevolumes			start
clinotty			download			node				restart				status
composer			fixowns				npm				root				stop
copyfromcontainer		fixperms			pwa-studio			rootnotty			update
copytocontainer			grunt				redis				setup				xdebug

These are the command that will be exicuted inside the Docker container. You may notice that we have things like npm and grunt. These will be our workhorse today.

Installing Grunt

There are going to be a few files that we need to work with before Grunt will install. Inside the root of our Magento install we have 3 files:

Gruntfile.js.sample
grunt-config.json.sample
package.json.sample

Right now there seems to be a little bug in the Docker project that it’s not coping files edited in the root over to the container. You need to copy these files (using the command line) but we are going to do this inside the container itself.

bin/bash
cp Gruntfile.js.sample Gruntfile.js
cp grunt-config.json.sample grunt-config.json
cp package.json.sample package.json
exit

Each of these files will have the configurations that we need for Grunt. You don’t need to look into the file’s content, as we will not be editing them (more on that in a second) but just so you know what they are doing, the Gruntfile.js is the entry point for all the commands that we will be using when we run Grunt. package.json is the file that will direct all the dependencies that we need to have installed to get everything working. And grunt-config…well…there is a little story there…

When we were first using Grunt in Magento we would edit the src/dev/tools/grunt/configs/themes.js file to add custom themes to the Grunt process. But in a way, that’s editing a core file. I.E. it’s not a good idea. So what grunt-config.json does is point to a file that we use to set our configurations for compiling our theme. This file can then be tracked by Git (since the core file is ignored). If you have looked into that file you will notice just a single line: {"themes": "dev/tools/grunt/configs/local-themes"}. So what this is telling us to do is to create a file in that location that we will add in our customizations. We will do this in a moment. But first…

Let’s install!

With our configuration files set, we need to install everything. From the root of your install:

bin/npm install grunt --save-dev
bin/npm install
bin/npm update
bin/npm install grunt-contrib-less

With these installed we just need to configure our custom theme settings. We will be using the same theme that we have used in other posts, but know that any theme will work here. Just change up the names, the format will stay the same.

Create a file: src/dev/tools/grunt/configs/local-themes.js. There we are going to copy over the code that is in src/dev/tools/grunt/configs/themes.js.

File: src/dev/tools/grunt/configs/local-themes.js

'use strict';

module.exports = {
    blank: {
        area: 'frontend',
        name: 'Magento/blank',
        locale: 'en_US',
        files: [
            'css/styles-m',
            'css/styles-l',
            'css/email',
            'css/email-inline'
        ],
        dsl: 'less'
    },
    luma: {
        area: 'frontend',
        name: 'Magento/luma',
        locale: 'en_US',
        files: [
            'css/styles-m',
            'css/styles-l'
        ],
        dsl: 'less'
    },
    theme: {
        area: 'frontend',
        name: 'BasicTheming/theme',
        locale: 'en_US',
        files: [
            'css/styles-m',
            'css/styles-l'
        ],
        dsl: 'less'
    },
    backend: {
        area: 'adminhtml',
        name: 'Magento/backend',
        locale: 'en_US',
        files: [
            'css/styles-old',
            'css/styles'
        ],
        dsl: 'less'
    }
};

Again, as of this writing, this file is not coping over to the Docker container correctly. So running this will create it in the container:

bin/bash
vi dev/tools/grunt/configs/local-themes.js
**paste in file content and save**
exit

If you took a look in themes.js you will have noticed that we have just added in our theme BasicTheming/theme in the same format as Magento/luma. The file is just pointing to the location of your theme, it’s namespace and the locale. If any of that is confusing to you, check out my post on theming. The files is the only things that needs a little explanation. This points to what all the Less in your theme will be compiled to. These are the files that will be read by the browser once the task is done.

The settings above are all you need to get up and running, but just to point out some of the other possibilities, look at the files setting for blank:

files: [
    'css/styles-m',
    'css/styles-l',
    'css/email',
    'css/email-inline'
],

Notice that there are more then just the base CSS files being compiled (styles-m and styles-l). This means you can point to custom CSS files that are written in Less, but compiled to be used outside of the theme. In this case, these are email files that are used when sending out order confirmations and the such. We wont dig any deeper here, but know there is more to this file then just what we have covered.

Thy Commandments

Technically, Grunt is capable of doing just about anything in your project. But outside of coding in those instructions yourself, there are built in commands that will cover all the processes we need.

bin/grunt clean

This command is pretty straight forward, it cleans the files that are in your temp areas (pub and var). What’s a little less clear is what exactly it’s clearing. The full list is outlined here: src/dev/tools/grunt/configs/clean.js. You will see in there you have a few options to clean just paths thats you wants. These include bin/grunt clean:var || :pub || :styles || :markup || :js. It’s worth keeping in mind that this cache clearing is faster then the default bin/magento c:c and might help clear out additional files that the core command misses.

bin/grunt exec

This command “Republishes symlinks to the source files” per the Magento docs. What this means for us is it recreates the foundation for the rest of our front end files. Under the hood (src/dev/tools/grunt/configs/exec.js) this file calls another utility process (in src/dev/tools/grunt/configs/combo.js) this in turn calls php bin/magento dev:source-theme:deploy. This process is essential in getting our system in order (as i will get into soon).

bin/grunt less

This command actually recompiles all our theme files in app and deploys them to the needed locations (mostly pub).

bin/grunt watch

This command just sets up a watcher to look to see if there are file changes in our theme. When there are, it quickly processes those changes and updates the needed files. This will be the workhorse of our tasks.

Bug in the system

As of this writing, there is a little bug in the compilation of styles in Magento. You might come across this:

Running "less:documentation" (less) task
>> SyntaxError: variable @_icon-font-text-hide is undefined in lib/web/css/docs/source/_icons.less on line 31, column 5:
>> 30 .example-icon-2 {
>> 31     .lib-icon-image(
>> 32         @_icon-image: '@{baseDir}images/blank-theme-icons.png',
Warning: Error compiling lib/web/css/docs/source/docs.less Use --force to continue.

Aborted due to warnings.

If this is the case, you can do a little core hack to skip over the compilation of the less:documentation. Inside src/dev/tools/grunt/configs/less.js you can edit like so:

...
var lessOptions = {
    options: {
        sourceMap: true,
        strictImports: false,
        sourceMapRootpath: '/',
        dumpLineNumbers: false, // use 'comments' instead false to output line comments for source
        ieCompat: false
    },
    setup: {
        files: {
            '<%= path.css.setup %>/setup.css': '<%= path.less.setup %>/_setup.less'
        }
    },
    updater: {
        files: {
            '<%= path.css.updater %>/updater.css': '<%= path.less.setup %>/_setup.less'
        }
    }
};
...

Running Grunt the right way

One of the frustrations people run into when using Grunt with Magento for the first time is the inconsistency they experience when setting up and running tasks. It’s poorly documented but you actually have to run your Grunt commends in order for them to work right. Running bin/grunt less won’t work if you haven’t run bin/grunt exec first. So now that we have covered all the commends we will be using, here is how I run commands to get everything going.

bin/grunt clean
bin/grunt exec
bin/grunt less
bin/grunt watch

This will work to get everything set right every time.

Wrapping up

And that’s about it. While there are a great number of things that Grunt can do for you while developing on Magento, this is the core functionality that will greatly speed up your workflow. At first it might seem like this is a lot of set up but once configured this workflow is priceless in speaking up your day.