Cordova

Cordova: CLI Hooks

I recently did a code review of a Cordova project for a student team. Surprisingly, things looks pretty clean with the code, but I informed the team that they needed to properly setup their Cordova hooks to automate their build process. Some of my recommendations included the following:

  • Automatically add the necessary plugins that your app uses on the “after_platform_add” event.
  • Cleanup and remove any debug, map or non-minified JavaScript or CSS files in your platform assets folder on the “after_prepare”.
  • Automatically setup the ant.properties file for Android to specify the key to use for signing your release.

With Cordova, it’s recommended to write these script files in JavaScript and use the node.js interpreter to execute them.

The following example script I created will go through the WWW folder and remove any “extra” files to help minimize the size of your package.

#!/usr/bin/env node
var fs = require('fs-extra');

//
// This hook removes the specified javascript files (non-minified)
//

var folders = [
  'assets/www/lib/css/ionic.css',
  'assets/www/lib/js/ionic.js',
  'assets/www/lib/js/ionic.bundle.js',
  'assets/www/lib/js/ionic-angular.js',
  'assets/www/lib/js/underscore.js',
  'assets/www/lib/js/angular/angular.js',
  'assets/www/lib/js/angular/angular.min.js.map',
  'assets/www/lib/js/angular/angular-animate.js',
  'assets/www/lib/js/angular/angular-animate.min.js.map',
  'assets/www/lib/js/angular/angular-cookies.js',
  'assets/www/lib/js/angular/angular-cookies.min.js.map',
  'assets/www/lib/js/angular/angular-loader.js',
  'assets/www/lib/js/angular/angular-loader.min.js.map',
  'assets/www/lib/js/angular/angular-resource.js',
  'assets/www/lib/js/angular/angular-resource.min.js.map',
  'assets/www/lib/js/angular/angular-route.js',
  'assets/www/lib/js/angular/angular-route.min.js.map',
  'assets/www/lib/js/angular/angular-sanitize.js',
  'assets/www/lib/js/angular/angular-sanitize.min.js.map',
  'assets/www/lib/js/angular/angular-scenario.js',
  'assets/www/lib/js/angular/angular-touch.js',
  'assets/www/lib/js/angular/angular-touch.min.js.map',
  'assets/www/lib/js/angular-ui/angular-ui-router.js'
];

var rootdir = process.argv[2];
var platforms = (process.env.CORDOVA_PLATFORMS || '').split(',');
folders.forEach(function(folder) {
  (platforms || []).forEach(function(platform) {
    var filename = rootdir+'/platforms/'+platform+'/'+folder;
    fs.delete(filename, function(err) {
      if (err) {
        return(console.log('Failed to remove file "'+filename+'". Error: '+err));
      }
      console.log('Successfully removed folder/file "'+filename +'"');
    });
  });
});

Works For Me…

The team quickly replied back that they were getting an error when trying to write their own scripts. The script wasn’t running with the following command line output:

cmd.exe /s /c "<path to project root>\hooks\<event name>\<hook script>" "<project root>"

We checked Cordova versions (4.3) and everything was the same between machines. I tested their project with one of my scripts and it worked as expected. However, when running their script, I was getting the same error. Looking over their hook script, everything looked fine.

However, when comparing the shell output between a working script and a failing script. I discovered that the wrong interpretor was being run. Node.js was not being called to process the file. Below is a successful call:

Running command: "C:\Program Files\nodejs\node.exe" "<project root>\hooks\after_prepare\010_purge.js" "<project root>"

I began to suspect that file encoding was the problem. The #! at the beginning of the file wasn’t being honored. To verify, I copied and pasted the contents of my working script into their script file and it failed. If I copied and pasted the contents of their file into my working script file it would work. Although that might have been a work-around for the problem, I wanted to know why this wasn’t working.

Not knowing how the file was originally created or what editor was used by the team, I created a new file with Notepad++ and pasted their contents into that file. Saved it and it ran with no problem. So my suspicions were confirmed. Whatever editor they saved the file with didn’t use the right text encoding.

I sent my findings back to the students where they verified that the file had been created with Visual Studio. Further comparison of the files showed that Visual Studio was saving the file with byte-order mark (BOM). Removing the BOM from the file when saving allows the CLI tools on Windows to select the proper shell (node.js).

HTML5

If you’re lucky enough to be able to switch to HTML5, you’ll be able to use the new built-in date picker controls.

<input type="date" id="mydate" value="2014-03-14" />

If you’re viewing this page with an HTML5 capable browser, the value will render as follows:

No special jQuery plugins or custom calendar controls necessary! With HTML5, there are a total of three different date and date/time picker controls:

  • date – The date (no timezone).
  • datetime – The date/time (with time zone).
  • datetime-local – The date/time (no timezone).

JavaScript Date/Time Parsing

If you’ve ever worked clientside with JavaScript, you’ll know that any new Date() function call will return a Date object that is localized based on the timezone set for the computer. You can also create a new date from an existing date or by parsing the string as follows:

  var date = new Date("Tue Mar 04 2014 16:00:00 GMT-0800");
  console.log(date);

With the above example, assuming you are located in CST (or a different timezone than PST), you’ll find the following output:

Tue Mar 04 2014 18:00:00 GMT-0600 (Central Standard Time)

Notice that the time has automatically corrected itself to the local timezone. OK, that’s good to know, but how does that affect me?

Try the following time:

  var date = new Date("Tue Mar 04 2014 00:00:00 GMT-0000");
  console.log(date);

Assuming you’re in the western hemisphere and you don’t live near the prime meridian, you should see something like the following:

Mon Mar 03 2014 18:00:00 GMT-0600 (Central Standard Time)

Did you catch that? The date has moved back to March 03 instead of March 04 because of the timezone conversion. OK, you get it, enough with the geography and math lessons… why should you care?

HTML5 Date Input

When using the <input type=”date” /> control, the value that is posted and returned from the input control is formatted as: YYYY-MM-DD. Since we have a date, we can assume that we can just let the JavaScript Date object parse it for us.

  var date = new Date('2014-03-04');
  // Mon Mar 03 2014 18:00:00 GMT-0600 (Central Standard Time)

What?!?! Why is it March 03 again? Lets try playing around with the Date object a little bit and see what happens.

  var date = new Date(2014, 2, 4); // Recall that month is 0 based, where 0=January
  // Tue Mar 04 2014 00:00:00 GMT-0600 (Central Standard Time)
  var date = new Date('03/04/2014');
  // Tue Mar 04 2014 00:00:00 GMT-0600 (Central Standard Time)

OK. Now that is what I’m expecting!!! So basically if we provide the string as YYYY-MM-DD, the Date object will parse that date automatically as a GMT time and then localize. However, if we pass it in as MM/DD/YYYY it will already be localized.

So how do we handle this for our HTML5 input control to get the date we expect? The following naive JavaScript will attempt to parse the input received and create a localized Date object. It will also attempt to parse a standard date in the MM/DD/YYYY format if HTML5 isn’t supported.

/**
 * Parses the date from the string input.
 * @param {Number|Date|string} date Teh value to be parsed.
 * @param {Function|Date} [defaultValue] The default value to use if the date cannot be parsed.
 * @returns {Date} The parsed date value. If the date is invalid or can't be parsed, then the defaultValue is returned.
 */
function parseDate(date, defaultValue) {
  if (! date) return(getDefaultValue());
  if (typeof(date) === 'date') return(date);
  if (typeof(date) === 'number') return(new Date(date));

  /**
   * Gets the default value.
   * @returns {Date}
   */
  function getDefaultValue() {
    return((typeof(defaultValue) === 'function') ? defaultValue(name) : defaultValue);
  }

  var results;
  // YYYY-MM-DD
  if ((results = /(\d{4})[-\/\\](\d{1,2})[-\/\\](\d{1,2})/.exec(date))) {
    return(new Date(results[1], parseInt(results[2], 10)-1, results[3]) || new Date(date) || getDefaultValue());
  }
  // MM/DD/YYYY
  if ((results = /(\d{1,2})[-\/\\](\d{1,2})[-\/\\](\d{4})/.exec(date))) {
    date = new Date(results[3], parseInt(results[1], 10)-1, results[2]) || new Date(date) || getDefaultValue();
  }
  return(new Date(date) || getDefaultValue());
}

So the next time you find yourself struggling with dates and times in JavaScript, be wary of the input parsing or the format of your input and / or timezone.

I’ve published my Active Directory authentication (AuthN) and authorization (AuthZ) module for node.js. This module supports large active directory installation where over 1000 entries may be returned from a query via range specifiers. In addition, the module will recursively enumerate and expand all nested users and groups.

You can view or checkout the code online on my github account:

Installation is easy with npm:

npm install activedirectory

Usage is pretty simple:

var ad = new ActiveDirectory('ldap://yourdomain.com', 'dc=yourdomain,dc=com', 'authuser@domain.com', 'authpassword');
var username = 'bob@domain.com';
ad.findUser(username, function(err, user) {
  if (err) {
    console.log('ERROR: ' +JSON.stringify(err));
    return;
  }

  if (! user) console.log('User: ' + username + ' not found.');
  else console.log(JSON.stringify(user));
});

Hope you find it useful!