Using grunt with Ext.js 4.x projects

17 March 2013 – Updated to included the new simpler syntax of 0.6.0

So I stumbled across a thread last week on the Sencha forums marked under their “Trending Threads”. It’s title:

Sencha Cmd Needs Some F*****G Coffee!!!

It’s an eye catching title! Specifically there was one post there which said:

I really would love to see someone starting a yeoman/grunt based community replacement for the Sencha CMD

Having written a POC of this at an old company I thought it right that I make a proper open source version and share it. So last week I hastily hacked up my first version and very prematurely posted it to the thread, twitter and here. It was crap, worked only on my project and was riddled with a lot of bad code and bugs (apologies if you tried it and hit problems).

Over the last few days I decided to tidy up the code, add some tests, and road test it on a few more project. I’m now happy to say that it’s working pretty solidly on Ext.js and Sencha Touch projects. So I’d like to run through the simple setup on an existing project. My choice was the Pandora MVC project that Sencha provide as their MVC walkthrough application, but the following steps should work just as well on any existing project your have.

If you get stuck or have feedback please leave a comment below.

Getting started on an existing project

I’m assuming you have node.js installed, if not grab a copy from http://nodejs.org/

First thing we need to do is add a package.json file to your project which can be done by running the following on the command line:

$ cd project/root/src/dir
$ npm init

This will ask a few questions about the project name, version, test commands, etc. For the ones where you know the data fill it in, for the others just hit enter and accept the defaults – they can always be changed later.

Next up you need to get the latest version of Grunt. To do this run the following:

$ npm uninstall -g grunt
$ npm install -g grunt-cli
$ npm install grunt --save-dev

You may need to run the first two as root, depending on your OS and previous installs
This should add grunt locally to your project. Running grunt should give:

$ grunt
A valid Gruntfile could not be found. Please see the getting started guide for
more information on how to configure grunt: http://gruntjs.com/getting-started
Fatal error: Unable to find Gruntfile.

Like Ant, make, etc they all need a file which defines some tasks to run. For grunt this file should be called Gruntfile.js and be in your project root. Here’s a simple one to get started with:

module.exports = function(grunt) {

    grunt.initConfig({
        
    });

    grunt.registerTask('default', []);

};

Hopefully if you run grunt again with the above in a file called Gruntfile.js you should see:

$ grunt

Done, without errors.

Adding the sencha_dependencies task

To add the sencha_dependencies task to your project you need to do two things. First add it as a dependency via NPM:

$ npm install grunt-sencha-dependencies --save-dev

And then we need to add it to our Gruntfile.js

module.exports = function(grunt) {

    grunt.initConfig({
        sencha_dependencies: {
          app: {
            options : {
              appFile: 'app/app.js',
              pageToProcess: 'index.html'
            }
          }
        }
    });

    grunt.loadNpmTasks('grunt-sencha-dependencies');

    grunt.registerTask('default', ['sencha_dependencies:app']);

};

For your project you’ll need to tweak the following:

  • appFile – This should be the relative path of the file containing your call to Ext.application from your html file
  • pageToProcess – This should be the name of the html file which we’ll use to run your app in headlessly and figure out what files get loaded and in what order you need them in production
  • pageRoot – By default the task assumes your html pages live in the root of your project. If thats not the case and they live under some subdirectory, then you should set this property to be your subdirectory

With the file now updated with the above try running:

$ grunt
Running "sencha_dependencies:app" (sencha_dependencies) task
Processing Sencha app file index.html...
>> Success! 230 files added to property sencha_dependencies_app

Done, without errors.

This tells you that it found 230 files which your app requires and it has now written it to a new global property called sencha_dependencies_app

If you wanted to see what files it found then you can pass in the --verbose flag and you should see something like:

$ grunt --verbose
<snip>
>> Success! 225 files added to property sencha_dependencies_app
Files are:
    ../libs/ext-4.1.1a/ext-debug.js
    ../libs/ext-4.1.1a/src/util/Observable.js
    ../libs/ext-4.1.1a/src/app/Controller.js
    ../libs/ext-4.1.1a/src/util/Filter.js
    ../libs/ext-4.1.1a/src/util/AbstractMixedCollection.js
    ../libs/ext-4.1.1a/src/util/Sorter.js
    ../libs/ext-4.1.1a/src/util/Sortable.js
    ../libs/ext-4.1.1a/src/util/MixedCollection.js
    ../libs/ext-4.1.1a/src/data/proxy/Proxy.js
    ../libs/ext-4.1.1a/src/data/Operation.js
<snip>
    ../libs/ext-4.1.1a/src/data/reader/Array.js
    ../libs/ext-4.1.1a/src/data/ArrayStore.js
    ../libs/ext-4.1.1a/src/data/Batch.js
    app/app.js

Done, without errors.

Using this with our tasks to create output files

On it’s own the above isn’t all that useful, but we can use the output of the task and pass it on to any other grunt task we want now. I think the most obvious example is wanting to concatenate and minify the code, in addition let’s also generate source maps to help with debugging in chrome. For this we need another task:

$ npm install grunt-contrib-uglify --save-dev

And in our Gruntfile.js

module.exports = function(grunt) {

    grunt.initConfig({
        sencha_dependencies: {
          app: {
            options : {
              appFile: 'app/app.js',
              pageToProcess: 'index.html'
            }
          }
        },
        uglify: {
          app: {
            options: {
              sourceMap: 'build/source-map.js'
            },
            files: {
              'build/app.min.js': ['<%= sencha_dependencies_app %>']
            }
          }
        }
    });
    
    
    grunt.loadNpmTasks('grunt-sencha-dependencies');
    grunt.loadNpmTasks('grunt-contrib-uglify');

    grunt.registerTask('default', ['sencha_dependencies:app', 'uglify:app]);

};

Now if you run grunt, you’ll see:

$ grunt
Running &quot;sencha_dependencies:app&quot; (sencha_dependencies) task
Processing Sencha app file index.html...
>&gt; Success! 230 files added to property sencha_dependencies_app

Running &quot;uglify:app&quot; (uglify) task
Source Map &quot;build/source-map.js&quot; created.
File &quot;build/app.min.js&quot; created.
Uncompressed size: 3488667 bytes.
Compressed size: 262655 bytes gzipped (890454 bytes minified).

Done, without errors.

The uglify task uses the files output from the sencha_dependencies task via the property it created:

<%= sencha_dependencies_app %>

This is special templating syntax which means the value in the property won’t be resolved until it’s needed. After that we can see uglify created a source-map.js which will work with debuggers that support them (chrome and some versions of firebug), and finally it concated and minifed all our files into dest/app.min.js

Using the minified file in our production application

Now we have a minified set of all the files we should make sure it gets used when we go live. I’ve seen teams in the past create an index-development.html and an index-production.html. This is one option, but grunt supports a copy tasks which can do any replacement we want, so let’s keep one index.html and assume we want to update it into our build output directory.

$ npm install grunt-contrib-copy --save-dev

Our grunt task will look like:

<snip>
    grunt.initConfig({
        <snip>
        copy: {
          app: {
            files: [
              {expand: true, src: ['resources/css/ext-all.css'], dest: 'build/', cwd: '../libs/ext-4.1.1a/',},
              {expand: true, src: ['resources/themes/images/**'], dest: 'build/', cwd: '../libs/ext-4.1.1a/',},
              {src: ['index.html'], dest: 'build/'}
            ],
            options: {
              processContent: function(content, filePath) {
                // process only the index.html content
                if (/index.html/.test(filePath)) {
                  // remove the ext script
                  content = content.replace(/<script.*ext.js"><\/script>/, '');
                  // now update the css location
                  content = content.replace(/\.\.\/libs\/ext-4.1.1a\//, '');
                  // now change our app.js to app.min.js
                  content = content.replace(/app\/app.js/, 'app.min.js');
                }
                return content;
              }
            }
          }
        }
    });

    grunt.loadNpmTasks('grunt-sencha-dependencies');
    grunt.loadNpmTasks('grunt-contrib-uglify');
    grunt.loadNpmTasks('grunt-contrib-copy');

    grunt.registerTask('default', ['sencha_dependencies:app', 'uglify:app', 'copy:app']);

};

This seems a little bit complicated but let me break it down:

  files: [
    {expand: true, src: ['resources/css/ext-all.css'], dest: 'build/', cwd: '../libs/ext-4.1.1a/',},
    {expand: true, src: ['resources/themes/images/**'], dest: 'build/', cwd: '../libs/ext-4.1.1a/',},
    {src: ['index.html'], dest: 'build/'}
  ]

The above will move the CSS and image files over to our build directory, and in our addition it will move our index.html over to the build directory.

  options: {
    processContent: function(content, filePath) {
      // process only the index.html content
      if (/index.html/.test(filePath)) {
        // remove the ext script
        content = content.replace(/<script.*ext.js"><\/script>/, '');
        // now change our app.js to app.min.js
        content = content.replace(/app\/app.js/, 'app.min.js');
        // now update the css location
        content = content.replace(/\.\.\/libs\/ext-4.1.1a\//, '');
      }
      return content;
    }
  }

This function is a hook the task provides to do… well pretty much anything! In my case I want to only process the index.html file and all I want to do is:

  • remove the reference to the ext.js library JavaScript file (it’s now in a minified file)
  • swap the app.js with our new minified app.min.j
  • and also update the url location of our css file to point to our build version

That’s it… this grunt file now does pretty much want any basic app needs.

More information

This is just a small walkthrough that will hopefully get people up and running with grunt on Ext.js projects. If you do have questions on the above please leave a comment below, or if you find a bug in the grunt-sencha-dependencies task please file an issue over at https://github.com/mattgoldspink/grunt-sencha-dependencies. Similarly, if there’s features you think would be useful then please let me know (though bear in mind I want to keep this task simple and just deal with dependencies).

In addition if you want to see a fuller example Grunt file on a project please take a look at the Pandora MVC example in the project – https://github.com/mattgoldspink/grunt-sencha-dependencies/tree/master/test/integration/pandora-ext-4.1.1a

4 thoughts on “Using grunt with Ext.js 4.x projects

  1. Hi,

    I’m trying to reproduce steps described in your article but I have problems in the ‘Adding the sencha_dependencies task’ step. After modifying the Gruntfile.js introducing what you describe, the grunt command fails with the following message:

    Loading “sencha_dependencies.js” tasks…ERROR
    >> Error: Cannot find module ‘.\lib\splitArrayIntoThree.js’
    Warning: Task “sencha_dependencies:app” not found. Use –force to continue.

    I have installed grunt-sencha-dependencies, I don’t understand the problem. Have you an idea of what is missing?

    Thanks in advance.
    Regards,
    Giuseppe

  2. Thanks for providing this. Any idea why I would be getting this error:

    Fatal error: ENOENT, no such file or directory ‘path/to/my/app/touch,

    Grunt out put is this:

    Running “sencha_dependencies:app” (sencha_dependencies) task
    Verifying property sencha_dependencies.app exists in config…OK
    File: [no files]
    Processing Sencha app file index.html…
    Reading ./index.html…OK
    Processing source…OK
    Writing ./701641.html…OK
    Fatal error: ENOENT, no such file or directory …

  3. Hi Jamie,

    Apologies for the delay in responding. I’ve been travelling for the last few weeks without internet access.

    It’s not immediately obvious, but I suspect you may have something misconfigured in the task. Can you head over to github page and create a new issue: https://github.com/mattgoldspink/grunt-sencha-dependencies/issues/new with details about your grunt config and the directory structure. Also if you could run with “grunt -v” and give the output, that will hopefully help me debug it.

    Thanks,
    Matt

Leave a Reply