Nov
09

Using Grunt with Pebble Build to create scalable PebbleKit JavaScript

posted on 09 November 2015 in programming with 0 Comments

Pebble watchapps communicate with the phone via PebbleKit, a sandboxed JavaScript environment that runs within the Pebble phone app. It’s pretty simple to setup the interactions on the JavaScript side, but what happens when the JavaScript starts to get more complex? Using Grunt we can manage a scalable JS app, including running linters, bundling and running unit tests.

Setting up Grunt

To get started with Grunt, you’ll need to install Node and NPM (Node Package Manager). If you don’t already have it installed, get it from Nodejs. With Node installed, go to your pebble project directory and type

$ npm init

This will launch an interactive prompt to help setup your package.json file for your project. Answer the questions as needed or just accept the defaults.

Next we need to install Grunt. Run this command to install the Grunt CLI globally.

$ npm install -g grunt-cli

We also need to add Grunt as a dependency for our project, to add your first dependency run

$ npm install grunt --save-dev

Notice that this time we didn’t pass the -g flag to install it globally, so by default it will be added as a local dependency. Open the package.json file and note that grunt is now included under devDependencies. All of our dependencies will be dev dependencies (specified by the --save-dev flag), simply put we’re saying that to build the project you’ll need this dependency, but the compiled app doesn’t depend on any node packages to run. Also take notice of the node_modules folder that’s appeared in your project directory, this is where npm stores the files for any project dependencies. Go ahead and add this folder to your .gitignore / .cvsignore file, you don’t want it checked into source control.

Next create a new Gruntfile.js file in the root of your project directory, this file will define the tasks we want to run when building the watchapp. Add this empty Grunt configuration to the new file:

module.exports = function(grunt) {

  // The (empty) default task.
  grunt.registerTask('default', []);

};

You can now check everything is setup properly by running this command from your project directory:

$ grunt

Done, without errors.

Adding JSHint to check code quality

Now let’s add our first build task to Grunt, we’ll add JSHint which will check the quality of our JavaScript according to rules you can customise. First, add the dependency to the project

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

Open the Gruntfile.js and update it to the following

module.exports = function(grunt) {

  grunt.initConfig({

    jshint: {
      files: [
        'Gruntfile.js', 
        'src/js/**/*.js'
      ],
      options: {
        jshintrc: true
      }
    },

  });

  grunt.loadNpmTasks('grunt-contrib-jshint');

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

};

The line grunt.loadNpmTasks('grunt-contrib-jshint'); is telling Grunt to reference the JSHint package and we’ve added jshint to the default build task grunt.registerTask('default', ['jshint']);.

The configuration of the task goes under grunt.initConfig({, you can find more details on how to customise the grunt-contrib-jshint task on NPM. In our example you’ll see we’re telling JSHint to check our Grunt file and any JavaScript file(s) in the src/js/ folder. jshintrc: true tells the task to look for a .jshintrc file somewhere in the folder heirarchy relative to the file we’re checking, this file defines our rules for JSHint, below is a sample your can use or you can see all the possible rules in the default configuration file. Save the following to a .jshintrc file in your project root folder:

/*
 * Example jshint configuration file for Pebble development.
 * Adapted from http://developer.getpebble.com/blog/2014/01/12/Using-JSHint-For-Pebble-Development/
 *
 * Check out the full documentation at http://www.jshint.com/docs/options/
 */
{
  // Declares the existence of a global 'Pebble' object
  "globals": { "Pebble" : true },

  // And standard objects (XMLHttpRequest and console)
  "browser": true,
  "devel": true,
  "node": true,

  // Do not mess with standard JavaScript objects (Array, Date, etc)
  "freeze": true,

  // Do not use eval! Keep this warning turned on (ie: false)
  "evil": false,

  /*
   * The options below are more style/developer dependent.
   * Customize to your liking.
   */

  // Do not allow blocks without { }
  "curly": false,

  // Prohibits the use of immediate function invocations without wrapping them in parentheses
  "immed": true,

  // Enforce indentation
  "indent": true,

  // Do not use a variable before it's defined
  "latedef": "nofunc",

  // Spot undefined variables
  "undef": "true",

  // Spot unused variables
  "unused": "true",
  
  // Require capitalization of all constructor functions e.g. `new F()`
  "newcap" : true,
  
  // Enforce use of single quotes (') everywhere
  "quotmark" : "single",
  
  // Tolerate use of `== null`
  "eqnull" : false,

  // Supress dot notation warnings (e.g. allow obj['prop'])
  "sub" : true
}

With the changes made, lets run Grunt again, you should see the following if there are no errors in your JavaScript files.

$ grunt

Done, without errors.

Bundling JavaScript

The next task we’ll add is bundling. This will combine all of our JavasScript files together into a single file, I do this because originally Pebble only supported a single file src/js/pebble-js-app.js, but with newer SDKs any JavaScript file in your src/js/ will be built, so this step is optional.

Bundling using Grunt is usually done with grunt-contrib-concat, followed by grunt-contrib-uglify if you also want to minify the result, you probably won’t want to minify your PebbleKit app however, because the reduction in size will be negligable (remember the JavaScript runs on your phone) but you’ll lose all benefit of debugging with log line numbers. For my project, I use grunt-browserify. This allows me to ignore the ordering / dependencies of the JavaScript files and just write JavaScript modules like a Node project, browserify will take care of the rest. Here’s an example of a browserify module:

var weather = require('./weather.js');

module.exports = {

  settings: {
    configVersion: 1,
    showWeather: true,
    weatherService: 'yahoo-weather'
  },

  isRunningInEmulator: function() {
    return Pebble.getActiveWatchInfo && /^qemu/.test(Pebble.getActiveWatchInfo().model);
  },

  loadConfig: function() {
    if (localStorage.config) {
      try {
        this.settings = JSON.parse(localStorage.config);
      } catch (e) { } 
    }

    weather.setWeatherService(this.settings.weatherService);
    console.log('Config loaded, using ' + weather.activeService.name + ' service');
  }

};

To add the task to our project, run

$ npm install grunt-browserify --save-dev

Open the Gruntfile.js and update it to the following

module.exports = function(grunt) {

  grunt.initConfig({

    jshint: {
      files: [
        'Gruntfile.js', 
        'src/js/**/*.js'
      ],
      options: {
        jshintrc: true
      }
    },

    browserify: {
      build: {
        src: [
          'src/js/**/*.js',
          '!src/js/pebble-js-app.js'
        ],
        dest: 'src/js/pebble-js-app.js'
      }
    }

  });

  grunt.loadNpmTasks('grunt-contrib-jshint');
  grunt.loadNpmTasks('grunt-browserify');

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

};

The src files are specified using Grunt globbing. src/js/**/*.js means find every JavaScript file in the /src/js/ folder and any subfolders, and the !src/js/pebble-js-app.js says don’t include the pebble-js-app.js file, which is our output file.

Generating an appinfo.h file

It can be very useful to have access to your appinfo.json data from your C code, like the name of the watchapp, the version or the list of appKeys, using Grunt this is a cinch. We’re going to use Grunt’s inbuilt templating facility to generate an appinfo.h file from a template. We’ll use the grunt-contrib-copy task for this, go ahead and add it to your project:

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

Next update your Gruntfile to copy a src/appinfo.tpl.h file to src/appinfo.h and process the file as a Grunt template (for brevity I’ve replaced our previous config with …).

module.exports = function(grunt) {

  var appConfig = {
    info: grunt.file.readJSON('appinfo.json')
  };

  grunt.initConfig({

    config: appConfig,

    copy: {
      main: {
        options: {
          process: function (content, path) {
            return grunt.template.process(content);
          }
        },
        files: {
          'src/appinfo.h': ['src/appinfo.tpl.h']
        }
      },
    },

    ...

  });

  grunt.loadNpmTasks('grunt-contrib-jshint');
  grunt.loadNpmTasks('grunt-browserify');
  grunt.loadNpmTasks('grunt-contrib-copy');

  grunt.registerTask('default', ['copy', 'jshint', 'browserify']);

};

Note the appConfig, we’re reading the appinfo.json file in so we can use the properties in our template file. Let’s create that template file in src/appinfo.tpl.h

#pragma once

#define LONG_NAME "<%= config.info.longName %>"
#define VERSION_LABEL "<%= config.info.versionLabel %>"
#define UUID "<%= config.info.uuid %>"

<% for (prop in config.info.appKeys) { 
  %>#define <%= prop %> <%= config.info.appKeys[prop] %>
<% } %>

If you now run our grunt command, you should find a sparkly new src/appinfo.h file that looks something like this:

#pragma once

#define LONG_NAME "PHD"
#define VERSION_LABEL "1.1"
#define UUID "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

#define KEY_TEMPERATURE 0
#define KEY_CONDITIONS 1
#define KEY_SHOW_WEATHER 2

Managing secret keys

API keys and usernames / passwords should be left out of your code for security reasons, a simple way to manage these using our Grunt setup is to creating a keys.json file in the root of the project. For JavaScript files, if you’re using Browserify you can simply include the keys like so:

var keys = require('../../../keys.json');

And if you’re generating a appinfo.h file, you can load the keys into the Grunt appConfig:

  var appConfig = {
    info: grunt.file.readJSON('appinfo.json'),
    keys: grunt.file.readJSON('keys.json')
  };

And use these in your appinfo.tpl.h file. e.g.

<% for (prop in config.keys) { 
  %>#define KEY_<%= prop %> <%= config.keys[prop] %>
<% } %>

Integrating Grunt into pebble build

Now that we’ve got a sweet Grunt setup, it’d be awesome if we could run the grunt task every time we did a pebble build. Well it turns out that it’s pretty simple, we just need to modify the wscript file in your project root. Add from sh import grunt under the first import file statement, then call grunt() within the def build(ctx): task.

import os.path
from sh import grunt

top = '.'
out = 'build'

def options(ctx):
    ctx.load('pebble_sdk')

def configure(ctx):
    ctx.load('pebble_sdk')

def build(ctx):
    ctx.load('pebble_sdk')

    grunt()

    build_worker = os.path.exists('worker_src')
    binaries = []

    for p in ctx.env.TARGET_PLATFORMS:
        ctx.set_env(ctx.all_envs[p])
        ctx.set_group(ctx.env.PLATFORM_NAME)
        app_elf='{}/pebble-app.elf'.format(ctx.env.BUILD_DIR)
        ctx.pbl_program(source=ctx.path.ant_glob('src/**/*.c'),
        target=app_elf)

        if build_worker:
            worker_elf='{}/pebble-worker.elf'.format(ctx.env.BUILD_DIR)
            binaries.append({'platform': p, 'app_elf': app_elf, 'worker_elf': worker_elf})
            ctx.pbl_worker(source=ctx.path.ant_glob('worker_src/**/*.c'),
            target=worker_elf)
        else:
            binaries.append({'platform': p, 'app_elf': app_elf})

    ctx.set_group('bundle')
    ctx.pbl_bundle(binaries=binaries, js=ctx.path.ant_glob('src/js/pebble-js-app.js'))

Also note on the last line we’re telling the build system to just use our single, bundled JavaScript file src/js/pebble-js-app.js.

Now when you run pebble build it will call our Grunt system, lint and bundle the JavaScript and generate an appinfo.h file.

All in a good day’s work.