Magento 2 is a great e-commerce platform, as it comes with many features built-in. It makes it easier for store owners to create a store that exactly fits their needs.

However, developing with Magento can be a pain, as it is not easy to learn, and even when you do, there is a lot of build or compilation required for a lot of minor changes, especially for front-end development.

Here's where Grunt comes in. Grunt is a Javascript task runner that helps in automating repeated tasks like compilation, minification, etc...

We'll go over how to use Magento's out-of-the-box commands in Grunt, and how to add our own to make our development faster.


Install Grunt-CLI

If you don't have grunt-cli installed globally, you need to install it with the following command:

npm install -g grunt-cli

If you get an error that permission is denied, you'll need to run the command as sudo or on Windows, you'll need to run PowerShell or Command Prompt as an administrator.


Setting up Magento with Grunt

If you look at your Magento project directory, you'll see that in the root there are two files: package.json and package.json.sample. If you open package.json, it will be just an empty JSON object like this:

{}

But if you open package.json.sample, you'll find a proper package.json that will look something like this:

{
    "name": "magento2",
    "author": "Magento Commerce Inc.",
    "description": "Magento2 node modules dependencies for local development",
    "license": "(OSL-3.0 OR AFL-3.0)",
    "repository": {
        "type": "git",
        "url": "https://github.com/magento/magento2.git"
    },
    "homepage": "http://magento.com/",
    "devDependencies": {
        "glob": "~7.1.1",
        "grunt": "~1.0.1",
        "grunt-autoprefixer": "~3.0.4",
        "grunt-banner": "~0.6.0",
        "grunt-continue": "~0.1.0",
        "grunt-contrib-clean": "~1.1.0",
        "grunt-contrib-connect": "~1.0.2",
        "grunt-contrib-cssmin": "~2.2.1",
        "grunt-contrib-imagemin": "~2.0.1",
        "grunt-contrib-jasmine": "~1.1.0",
        "grunt-contrib-less": "~1.4.1",
        "grunt-contrib-watch": "~1.0.0",
        "grunt-eslint": "~20.1.0",
        "grunt-exec": "~3.0.0",
        "grunt-jscs": "~3.0.1",
        "grunt-replace": "~1.0.1",
        "grunt-styledocco": "~0.3.0",
        "grunt-template-jasmine-requirejs": "~0.2.3",
        "grunt-text-replace": "~0.4.0",
        "imagemin-svgo": "~5.2.1",
        "load-grunt-config": "~0.19.2",
        "morgan": "~1.9.0",
        "node-minify": "~2.3.1",
        "path": "~0.12.7",
        "serve-static": "~1.13.1",
        "squirejs": "~0.2.1",
        "strip-json-comments": "~2.0.1",
        "time-grunt": "~1.4.0",
        "underscore": "~1.8.0"
    }
}

So, we'll remove the current package.json:

rm package.json

then rename the package.json.sample to package.json:

mv package.json.sample package.json

After that, we'll install the dependencies using NPM:

npm install

Once the installation is done, you'll have a new node_modules directory with all the dependencies we'll need to run grunt.

The next step would be to set up Gruntfile.js which holds the tasks to be run by Grunt. You'll find a Gruntfile.js.sample in the root directory, so we'll just have to rename it to Gruntfile.js:

mv Gruntfile.js.sample Gruntfile.js

By default in Magento, the following tasks are one of those defined in Grunt:

  1. default: Just shows a default message in the terminal.
  2. clean: cleans the directories that hold the cached or generated files.
  3. deploy: generates static files.
  4. refresh: cleans cache and refreshes the generated static files.

Usually, in Magento, when making changes in modules or themes to assets like Javascript, LESS, or CSS files, you'll need to run the following command to see the file changes in action:

php bin/magento setup:static-content:deploy -f

Magento claims that this is unnecessary in development, which is why we use -f, however, if you've used Magento you'll know that this is actually not true and you need to run this command whenever you need to see the changes you made.

Actually, prior to running that command, you'll probably also need to remove directories like var/cache or var/page_cache, or you'll need to run commands that clears and flushes the cache.

Running all these different commands can be such a hassle, and this is where Grunt comes in. You can do all that just with a simple command:

grunt refresh

This command first runs the clean command, which will clear up all the directories holding the cache, then will compile all themes and their assets.

Compiling Your Custom Theme

Grunt compiles the themes that are declared in dev/tools/grunt/configs/themes.js. The declaration for a theme looks something like this:

blank: {
        area: 'frontend',
        name: 'Magento/blank',
        locale: 'en_US',
        files: [
            'css/styles-m',
            'css/styles-l',
            'css/email',
            'css/email-inline'
        ],
        dsl: 'less'
    },

As you can see, the key of the object is the name of the theme. It has the following properties:

  1. area: Where the theme applies. Can be frontend, adminhtml, or doc.
  2. name: The name of the theme, which is in the format VENDOR_THEME.
  3. locale: The locale of the theme.
  4. files: These are the files in the theme that should be compiled.
  5. dsl: stands for dynamic stylesheet language and can be either less or sass

So, to make sure your theme is compiled when running grunt refresh, you'll need to add your theme to this object, with the values for the properties based on your theme.


Adding Custom Tasks

The next part we'll go over is how to create custom tasks in Grunt that will help make our development faster.

One use case we can automate is when we update a module's version. Usually, you have to run 3 different tasks:

php bin/magento setup:upgrade
php bin/magento setup:di:compile
php bin/magento setup:static-content:deploy -f

We'll create a new task called upgrade that will run all these tasks for us.

To create a new task, you need to create a new file in dev/tools/grunt/tasks. We'll create a new file there called upgrade.js.

The file should export a function that takes grunt as a parameter:

module.exports = function(grunt) {
	//TODO code for the task
};

the reason behind this is that Gruntfile.js fetches all files in the tasks directory, and passes them the instance of grunt.

Next, we'll declare some functions that will be helpful for us:

const exec = require('child_process').execSync,
        log = grunt.log.write,
        ok = grunt.log.ok
  1. exec: it's actually the function execSync which allows us to run commands we would run in shell. We'll use it to run the commands mentioned above.
  2. log: A function that allows us to output information messages.
  3. ok: A function that allows us to output successful messages.

Next, to register our task, we'll use grunt.registerTask which takes two parameters: the name of the task and the function that the task will execute once called.

grunt.registerTask('upgrade', function () {
});

The first thing the task should do is run the command php bin/magento setup:upgrade. We'll use exec to run it:

log('Running setup:upgrade...')
exec('php bin/magento setup:upgrade', {stdio: 'inherit'})

The first parameter is the command to run, and the second parameter is an options object. The option we're passing is stdio with the value inherit, which means that the output should be printed to the terminal we're calling the task from.

Next, we need to run the command php bin/magento setup:di:compile. We'll also use exec to do that:

log('Running setup:di:compile')
exec('php bin/magento setup:di:compile', {stdio: 'inherit'})

Lastly, we need to run the command php bin/magento setup:static-content:deploy -f. Instead of running it through exec, we'll run another grunt task which is the deploy task since it's already declared and does all the work for us:

log('Running deploy...')
grunt.task.run('deploy')
ok('Upgrade finished!')

We run a grunt task with grunt.task.run passing it the name of the task. In the end, we're just outputting a successful message to show that our task is done.

That's it! upgrade.js should look like this:

module.exports = function(grunt) {
    const exec = require('child_process').execSync,
        log = grunt.log.write,
        ok = grunt.log.ok

    grunt.registerTask('upgrade', function () {
        log('Running setup:upgrade...')
        exec('php bin/magento setup:upgrade', {stdio: 'inherit'})
        log('Running setup:di:compile')
        exec('php bin/magento setup:di:compile', {stdio: 'inherit'})
        log('Running deploy...')
        grunt.task.run('deploy')
        ok('Upgrade finished!')
    });
}

Let's test it out. In the terminal run:

grunt upgrade

If everything is done correctly, the task will run all 3 commands. This task will make it easier next time you need to upgrade because of a new or updated module!

Now, you can automate any task with the same process. Create a file in dev/tools/grunt/tasks that exports a function that takes grunt as a parameter. Inside the function, register the task using grunt.registerTask and you can add whatever functionality you need inside.


Conclusion

Using Grunt with Magento, you can automate so many tasks that will make your development faster and easier! I suggest that once you start using Grunt with Magento, you keep all the tasks you create for all your projects, as it will be very beneficial.