Tuesday 11 February 2014

Listing Windows Azure Mobile Services NPM Packages

It's useful to know what NPM packages are pre-installed and available to use out-of-the-box in our Mobile Services. I thought it might be good to get a list of these packages and decided to create an API method which listed the packages and dependencies, with a description of what they are.

NPM list Command

The list (or ls for short) command lists the packages and can be called using the child_process.exec command. This function does this and starts examining the packages:

function npmls() {
        var exec = require('child_process').exec;
   
        // In node you can use --long switch to show description but it causes maxBuffer exceeded on parse
        var child = exec('npm ls --json',
          function (error, stdout, stderr) {
           
            var tree = JSON.parse(stdout);
            npminfo(tree, '');
           
            setNpmHttp(dequeuePackage);

            if (error !== null) {
              console.log('exec error: ' + error);
            }
        });
    }

The JSON looks like this if we log it:

{
  "problems": [
    "invalid: msnodesql@0.2.1 D:\\home\\site\\wwwroot\\node_modules\\sqlserver"
  ],
  "dependencies": {
    "apn": {
      "version": "1.3.8",
      "dependencies": {
        "q": {
          "version": "0.9.6"
        }
      }
    },
    "azure": {
      "version": "0.6.7-zumo",
      "from": "https://github.com/WindowsAzure/azure-sdk-for-node/tarball/v0.6.7-zumo",
      "dependencies": {
        "azure": {
          "version": "0.7.15",
          "from": "git://github.com/WindowsAzure/azure-sdk-for-node.git#v0.7.15-August2013",
          "dependencies": {
            "xml2js": {
              "version": "0.4.0",
              "dependencies": {
                "sax": {
                  "version": "0.5.5"
                }
              }
            },
            "request": {
              "version": "2.25.0",
              "dependencies": {
                "qs": {
                  "version": "0.6.5"
                },
                "json-stringify-safe": {
                  "version": "5.0.0"
                },
                "forever-agent": {
                  "version": "0.5.0"
                },
                "tunnel-agent": {
                  "version": "0.3.0"
                },
                "http-signature": {
                  "version": "0.10.0",
                  "dependencies": {
                    "assert-plus": {
                      "version": "0.1.2"
                    },

HTTPS Certificate Errors

This method was needed to make the npm view GET use http instead of https due to certificate trust issues:

// Used http instead of https to stop certificate errors
    function setNpmHttp (callback) {
       
        var exec = require('child_process').exec;
   
            var child = exec('npm config set registry="http://registry.npmjs.org/"',
              function (error, stdout, stderr) {
                callback();
            });      
    }

NPM Info

Once we've parsed the JSON, we can start examining each package and it's dependencies. This function walks the dependencies and builds an array of objects used for processing the package information:

// Recursive function for getting package info for all dependencies
    function npminfo(node, depth) {
         
        var dp = node.dependencies;
        if(dp !== null) {      
            for (var key in dp) {
              if (dp.hasOwnProperty(key)) {                      
               
                // Build info
                var link = 'https://www.npmjs.org/package/' + key;
                var info = depth + '<a href=\'' + link + '\', \'' + key + '\' onclick="window.open(this.href, \'' + key + '\' ); return false" target="_blank">' + key + '</a>' + ' V' + dp[key].version;
               
                packages.push({ 'key' : key, 'info' : info, 'depth' : depth.length })
                               
                // Loop through dependencies
                npminfo(dp[key], depth + '&#45;');
              }
            }
        }
    }

Extra Information

Once we have a list of package objects, we can process them to get extra description information:

function dequeuePackage(){
        // Dequeue and execute
        if(packages.length > 0){
            var pack = packages.shift();
            if(pack.depth == 0){
               
                writeLine();
               
                // Get detail
                var exec = require('child_process').exec;
       
                var child = exec('npm view ' + pack.key + ' description',
                  function (error, stdout, stderr) {
                     
                    if (error !== null) {
                      console.error('exec error: ' + error);
                    }
                    else{          
                        pack.info = pack.info + ' - ' + stdout;                        
                        writeLine('<b>' + pack.info + '</b>');
                    }
                   
                    // Do next
                    dequeuePackage();
                });
            }
            else {
                // Do next
                writeLine(pack.info);
                dequeuePackage();
            }
        }
        else {      
            // We're finished      
            opText = opText + '</body></html>';
           
            response.send(statusCodes.OK, opText);
        }  
    }

This is only done for top-level packages because it take's too long to make requests for all packages and the API request will time-out!

Full Script

When we put it all together, the API method looks like this:

exports.get = function(request, response) {
   
    // Array of package info objects
    var packages = [];
   
    // html start
    var opText = '<!DOCTYPE html><html><head><title>NPM Modules</title></head><body>';
    opText += '<h1>Windows Azure Mobile Services</h1>';
    opText += '<h2>NPM Packages and Dependencies</h2>';
   
    // List packages
    npmls();
   
    // Adds a line with break
    function writeLine(text){
        if(text)
            opText += text;
        opText += '<br />';
    }
   
    function npmls() {
        var exec = require('child_process').exec;
   
        // In node you can use --long switch to show description but it causes maxBuffer exceeded on parse
        var child = exec('npm ls --json',
          function (error, stdout, stderr) {
           
            var tree = JSON.parse(stdout);
            console.log(stdout);
            npminfo(tree, '');
           
            setNpmHttp(dequeuePackage);

            if (error !== null) {
              console.log('exec error: ' + error);
            }
        });
    }
   
    // Used http instead of https to stop certificate errors
    function setNpmHttp (callback) {
       
        var exec = require('child_process').exec;
   
            var child = exec('npm config set registry="http://registry.npmjs.org/"',
              function (error, stdout, stderr) {
                callback();
            });
       
    }
   
    // Recursive function for getting package info for all dependencies
    function npminfo(node, depth) {
         
        var dp = node.dependencies;
        if(dp !== null) {      
            for (var key in dp) {
              if (dp.hasOwnProperty(key)) {                      
               
                // Build info
                var link = 'https://www.npmjs.org/package/' + key;
                var info = depth + '<a href=\'' + link + '\', \'' + key + '\' onclick="window.open(this.href, \'' + key + '\' ); return false" target="_blank">' + key + '</a>' + ' V' + dp[key].version;
               
                packages.push({ 'key' : key, 'info' : info, 'depth' : depth.length })
                               
                // Loop through dependencies
                npminfo(dp[key], depth + '&#45;');
              }
            }
        }
    }
   
    function dequeuePackage(){
        // Dequeue and execute
        if(packages.length > 0){
            var pack = packages.shift();
            if(pack.depth == 0){
               
                writeLine();
               
                // Get detail
                var exec = require('child_process').exec;
       
                var child = exec('npm view ' + pack.key + ' description',
                  function (error, stdout, stderr) {
                     
                    if (error !== null) {
                      console.error('exec error: ' + error);
                    }
                    else{          
                        pack.info = pack.info + ' - ' + stdout;                        
                        writeLine('<b>' + pack.info + '</b>');
                    }
                   
                    // Do next
                    dequeuePackage();
                });
            }
            else {
                // Do next
                writeLine(pack.info);
                dequeuePackage();
            }
        }
        else {      
            // We're finished      
            opText = opText + '</body></html>';
           
            response.send(statusCodes.OK, opText);
        }  
    }
};

Finally

You can test it out here (although I may take it off-line at some point!).

No comments:

Post a Comment