Bower

Building an Organized, Automated, and Sustainable Workflow Using Bower, Grunt, and Github

+

Web Dev at the University of Alabama Libraries

Optimal Dev Flow

The Team

  • 3 Developers
  • 4 Web Librarians
  • Wide range of tech backgorunds
  • Sustainable
  • Automated
  • Configurable
  • Gentle learning curve

Local

Server

Manual source management

Working on your own

Local

Server

Manual dependency management

Working on your own

Working on your own

Manual Management

of Source & Dependencies 

Straight forward 

enough

Until you start

working with others

Working in a team

Manual Source Management

You

Server

Working in a team

Manual Source Management

Teammate

Server

Working in a team

Manual Source Management

The Ritual

Update from

server

Edit

Upload

Double check if file has been updated

Version Control System

  • Source is a versioned repository
  • Merge or preserve changes
  • Tracks changes
  • Revert or rollback
  • User permissions

Graphic from Git website - https://git-scm.com

Versioning System

Merge Changes

Commits from other devs

Commits you made

Merges changes to repository for you

Versioning System

Tracks Changes

Versioning System

Distributed Repositories

Versioning System

Flexible Repository Structure

branch

misc

tags

trunk

dist

src

assets

app

api

index.html

git-scm.com

Met our needs

  1. Distributed versioning system
  2. Commonly used
  3. Flexible repository structure
  4. Content VCS - only content stored
  5. SUSE Enterprise supported it...

"gitflow" branching mode image by Vincent Driessen

http://nvie.com/posts/a-successful-git-branching-model/

Complicated branching can seem convoluted and intimidating.

Branching Models

Each clone is a branch. Each clone can have branches in it.

Simple Branching Model

  1. Single origin
  2. Single branch
  3. Distributed clones

Central origin, Distributed clones

Living on the server

  • Git server [git://]
  • Security & authentication
  • User permissions

Ugh, even more stuff I have to learn?

Github

github.com

Github

  • Contributors
  • Teams
  • Permissions
  • Admins

Organization Account

Github

  • Repository & commit browsing
  • Statistics
  • Wiki
  • Bug reporting
  • much more

Many useful features

What about dependencies?

Separate from your repos, but integral to development

Dependencies

Jayne

Manual management in a team

You

two weeks later

Dependencies

Manual management with a team

Jayne

didn't check they version

A package manager for the web.

 

Bower

bower.io

nodejs.org

JavaScript runtime platform

build on V8 (Chrome JS engine)

Bower

Automate Dependency Management

# registered package
$ bower install angular

# GitHub shorthand <github-user>/<repo-name>
$ bower install jquery/jquery

# Github URL
$ bower install https://github.com/FortAwesome/Font-Awesome.git

# Git endpoint
$ bower install git://YOUR-GIT-SERVER.com/package.git

# URL
$ bower install http://example.com/script.js
  • Single command install
  • Specify versions
  • Use public and private servers

Project Configuration

$ bower install
{
  "name": "roots-ualib",
  "version": "2.1.2",
  "homepage": "https://github.com/ualibweb/roots-ualib",
  "license": "ECL-2.0",
  "private": true,
  "ignore": [
    "**/.*",
    "node_modules",
    "assets/vendor"
  ],
  "dependencies": {
    "modernizr": "2.8.2",
    "jquery": "1.11.1",
    "bootstrap": "3.3.2",
    "respond": "1.4.2",
    "angular-filter": "~0.5.4",
    "ng-file-upload": ">=4",
    "yamm3": "https://github.com/geedmo/yamm3.git#~1.0.0",
    "ualib-ui": "https://github.com/ualibweb/ui-components.git",
    "onesearch": "https://github.com/ualibweb/oneSearch_ui.git",
    "ualib-hours": "https://github.com/ualibweb/hours_ui.git",
    "manage": "https://github.com/ualibweb/manage_ui.git",
    "databases": "https://github.com/ualibweb/databases_ui.git",
    "musicSearch": "https://github.com/ualibweb/musicSearch_ui.git",
    "ualib_staffdir": "https://github.com/ualibweb/staffdir_ui.git",
    "ualib-softwareList": "https://github.com/ualibweb/softwareList_ui.git",
    "ualib-news": "https://github.com/ualibweb/news_ui.git",
    "angular-carousel": "0.3.12",
    "angular-ui-tinymce": "latest"
  },
  "devDependencies": {
    "fontawesome": "~4.3.0",
    "angular": ">=1 <1.3.0",
    "angular-route": ">=1 <1.3.0",
    "angular-resource": ">=1 <1.3.0",
    "angular-animate": ">=1 <1.3.0",
    "angular-sanitize": ">=1 <1.3.0"
  },
  "resolutions": {
    "angular": ">=1 <1.3.0",
    "angular-route": ">=1 <1.3.0",
    "angular-resource": ">=1 <1.3.0",
    "angular-animate": ">=1 <1.3.0",
    "angular-sanitize": ">=1 <1.3.0"
  }
}

bower.json

Bower

Getting Organized

  • Inconvenient
  • Search to find feature
  • Some features go unnoticed

Catch All

Getting Organized

  • Separation of concerns
  • Each unique feature is packaged separately
  • Features easier to find

Modularization

Grunt

gruntjs.com

The JavaScript Task Runner

nodejs.org

JavaScript runtime platform

build on V8 (Chrome JS engine)

Concatenate

Grunt

Uglify

/**
 * Bento Service Provider
 *
 * This service uses the mediaTypes service to organize the engine results by media type
 * and preloaded an engine's template and controller (if defined) if there are results for that engine.
 */
    .service('Bento', ['$routeParams', '$rootScope','$q', 'oneSearch', 'mediaTypes', function($routeParams, $rootScope, $q, oneSearch, mediaTypes){
        //variable representing 'this' for closure context
        //this ensures function closure reference variables in the right context
        var self = this;

        /**
         * Object to hold box data
         * @Object = {
         *      // Box names are generated by media types defined by registered engines
         *      NAME: {
         *          // An Array of engine names (String) used to get engine templates/controllers and track when the box is done loading.
         *          // Once an engine is finished loading, the engine's name is removed from this array. The removed value is used to reference the loaded engine's preloaded
         *          // template/controller. Once this Array is empty the "box" is considered loaded.
         *          engines: Array,
         *
         *          // The object's keys are the engine names and the values are the results returned using
         *          // the JSON path from an engine's resultsPath param
         *          results: {
         *              ENGINE_NAME1: {},
         *              ENGINE_NAME2: {},
         *              etc...
         *          }
         *      }
         *  }
         */
        this.boxes = {};

        this.boxMenu = [];

        /**
         * Object to hold pre-loaded engine templates and controllers.
         * Templates and controllers are only pre-loaded if the engine yields results.
         * @Object = {
         *      // Engine name, defined by engine's config registering with the oneSearchProvider
         *      NAME: {
         *          // the "tpl" key returns a Promise to retrieve the engine's template
         *          // The Promise is generated from Angular's $http Service (https://code.angularjs.org/1.3.0/docs/api/ng/service/$http),
         *          // which uses the promise methods from Angular's $q Service (https://code.angularjs.org/1.3.0/docs/api/ng/service/$q)
         *          tpl: Promise,
         *
         *          // The "controller" key will return an instance of the engine's controller or "null" if no controller was defined
         *          controller: Controller Instance|null
         *      }
         * }
         */
        this.engines = {};

        // Helper function that removes an engine's name from a box's "engines" Array
        // Once the "engines" Array is empty, the box is considered "loaded"
        function loadProgress(type, engine){
            var i = self.boxes[type].engines.indexOf(engine);
            if(i != -1) {
                setResultLimit(type);
                self.boxes[type].engines.splice(i, 1);
            }
        }

        // Remove an engine from all boxes
        function removeFromBoxes(engine){
            angular.forEach(self.boxes, function(box, type){

                loadProgress(type, engine);
            })
        }

        function initResultLimit(box){
            var numEngines = self.boxes[box]['engines'].length;
            var limit = numEngines > 1 ? 1 : (numEngines < 2 ? 3 : 2);
            self.boxes[box].resultLimit = limit;
        }

        function setResultLimit(box){
            $q.when(self.boxes[box].results)
                .then(function(results){
                    var numResults = Object.keys(results).length;
                    var numEngines = self.boxes[box]['engines'].length;
                    var expecting = numResults + numEngines;

                    if ((expecting < 2 && self.boxes[box].resultLimit < 3) || (expecting < 3 && self.boxes[box].resultLimit < 2)){
                        self.boxes[box].resultLimit++;
                    }
                });
        }

        var engines;

        // Gets all boxes
        this.getBoxes = function(){
            // Search all engines registered with the oneSearch Provider, giving the
            // $routeParams object as the parameter (https://code.angularjs.org/1.3.0/docs/api/ngRoute/service/$routeParams)
            engines = oneSearch.searchAll($routeParams);

            // Deep copy media types defined by registered engines to the this.boxes object.
            angular.copy(mediaTypes.types, self.boxes);

            // Pre-define the "results" object for each media type - I only do this here so I don't have to check if it's defined later
            angular.forEach(self.boxes, function(box, type){
                initResultLimit(type);
                self.boxes[type].results = {};
                self.boxes[type].resourceLinks = {};
                self.boxes[type].resourceLinkParams = {};

            });

            //  Iterate over the Promises for each engine returned by the oneSearch.searchAll() function
            angular.forEach(engines, function(engine, name){

                engine.response
                    .then(function(data){ // If $http call was a success

                        // User the engine's results getter to get the results object
                        // The results getter is defined by the JSON path defined by the
                        // "resultsPath" param in an engine's config
                        var res = engine.getResults(data);
                        var link = engine.getResourceLink(data);

                        // Double check that the data is defined, in case the search API returned a '200' status with empty results.
                        if (isEmpty(res)){
                            //console.log(self.boxes);
                            removeFromBoxes(name);
                            //console.log(self.boxes);
                        }
                        else {
                            res = res.map(function(item, i){
                                var newItem = item;
                                newItem.position = i;
                                return newItem;
                            });
                            //console.log(res);
                            // Group the results by defined media types
                            var grouped = mediaTypes.groupBy(res, engine.mediaTypes);

                            // Iterate over the boxes.
                            Object.keys(self.boxes).forEach(function(type){
                                // If a box type matches a group in the grouped results
                                if (grouped.hasOwnProperty(type)){
                                    // Put results in the boxes "results" object, referenced by the engine's name
                                    // Ex: self.boxes['books'].results['catalog'] = group_result;
                                    //
                                    // Also, limit the number of results per group by 3
                                    // and sort by generation position in the original results list
                                    self.boxes[type].results[name] = grouped[type].sort(function(a, b){
                                        if (a.position > b.position){
                                            return 1;
                                        }
                                        if (a.position < b.position){
                                            return -1;
                                        }
                                        return 0;
                                    });

                                    // set resource "more" link
                                    self.boxes[type].resourceLinks[name] = decodeURIComponent(link[engine.id]);

                                    // set resource link parameters by media type specified by the engine config
                                    if (angular.isObject(engine.mediaTypes)){
                                        self.boxes[type].resourceLinkParams[name] = engine.mediaTypes.types[type];
                                    }
                                }
                                // update loading progress, setting engine as loaded for current box
                                loadProgress(type, name);
                            });

                            //preload the engine's template for easy access for directives
                            self.engines[name] = {};
                            self.engines[name].tpl = oneSearch.getEngineTemplate(engine);
                            self.engines[name].controller = oneSearch.getEngineController(engine);
                        }
                    }, function(msg){
                        // If error code return from $http, iterate through boxes object
                        // and remove any instance engine from a box's "engines" array
                        removeFromBoxes(name);
                    });
            });

        }
    }])
.service("Bento", [ "$routeParams", "$rootScope", "$q", "oneSearch", "mediaTypes", function(a, b, c, d, e) {
    function f(a, b) {
        var c = j.boxes[a].engines.indexOf(b);
        -1 != c && (i(a), j.boxes[a].engines.splice(c, 1));
    }
    function g(a) {
        angular.forEach(j.boxes, function(b, c) {
            f(c, a);
        });
    }
    function h(a) {
        var b = j.boxes[a].engines.length, c = b > 1 ? 1 : 2 > b ? 3 : 2;
        j.boxes[a].resultLimit = c;
    }
    function i(a) {
        c.when(j.boxes[a].results).then(function(b) {
            var c = Object.keys(b).length, d = j.boxes[a].engines.length, e = c + d;
            (2 > e && j.boxes[a].resultLimit < 3 || 3 > e && j.boxes[a].resultLimit < 2) && j.boxes[a].resultLimit++;
        });
    }
    var j = this;
    this.boxes = {}, this.boxMenu = [], this.engines = {};
    var k;
    this.getBoxes = function() {
        k = d.searchAll(a), angular.copy(e.types, j.boxes), angular.forEach(j.boxes, function(a, b) {
            h(b), j.boxes[b].results = {}, j.boxes[b].resourceLinks = {}, j.boxes[b].resourceLinkParams = {};
        }), angular.forEach(k, function(a, b) {
            a.response.then(function(c) {
                var h = a.getResults(c), i = a.getResourceLink(c);
                if (isEmpty(h)) g(b); else {
                    h = h.map(function(a, b) {
                        var c = a;
                        return c.position = b, c;
                    });
                    var k = e.groupBy(h, a.mediaTypes);
                    Object.keys(j.boxes).forEach(function(c) {
                        k.hasOwnProperty(c) && (j.boxes[c].results[b] = k[c].sort(function(a, b) {
                            return a.position > b.position ? 1 : a.position < b.position ? -1 : 0;
                        }), j.boxes[c].resourceLinks[b] = decodeURIComponent(i[a.id]), angular.isObject(a.mediaTypes) && (j.boxes[c].resourceLinkParams[b] = a.mediaTypes.types[c])), 
                        f(c, b);
                    }), j.engines[b] = {}, j.engines[b].tpl = d.getEngineTemplate(a), j.engines[b].controller = d.getEngineController(a);
                }
            }, function() {
                g(b);
            });
        });
    };
} ])

Grunt

Grunt

Minify

.service("Bento", [ "$routeParams", "$rootScope", "$q", "oneSearch", "mediaTypes", function(a, b, c, d, e) {
    function f(a, b) {
        var c = j.boxes[a].engines.indexOf(b);
        -1 != c && (i(a), j.boxes[a].engines.splice(c, 1));
    }
    function g(a) {
        angular.forEach(j.boxes, function(b, c) {
            f(c, a);
        });
    }
    function h(a) {
        var b = j.boxes[a].engines.length, c = b > 1 ? 1 : 2 > b ? 3 : 2;
        j.boxes[a].resultLimit = c;
    }
    function i(a) {
        c.when(j.boxes[a].results).then(function(b) {
            var c = Object.keys(b).length, d = j.boxes[a].engines.length, e = c + d;
            (2 > e && j.boxes[a].resultLimit < 3 || 3 > e && j.boxes[a].resultLimit < 2) && j.boxes[a].resultLimit++;
        });
    }
    var j = this;
    this.boxes = {}, this.boxMenu = [], this.engines = {};
    var k;
    this.getBoxes = function() {
        k = d.searchAll(a), angular.copy(e.types, j.boxes), angular.forEach(j.boxes, function(a, b) {
            h(b), j.boxes[b].results = {}, j.boxes[b].resourceLinks = {}, j.boxes[b].resourceLinkParams = {};
        }), angular.forEach(k, function(a, b) {
            a.response.then(function(c) {
                var h = a.getResults(c), i = a.getResourceLink(c);
                if (isEmpty(h)) g(b); else {
                    h = h.map(function(a, b) {
                        var c = a;
                        return c.position = b, c;
                    });
                    var k = e.groupBy(h, a.mediaTypes);
                    Object.keys(j.boxes).forEach(function(c) {
                        k.hasOwnProperty(c) && (j.boxes[c].results[b] = k[c].sort(function(a, b) {
                            return a.position > b.position ? 1 : a.position < b.position ? -1 : 0;
                        }), j.boxes[c].resourceLinks[b] = decodeURIComponent(i[a.id]), angular.isObject(a.mediaTypes) && (j.boxes[c].resourceLinkParams[b] = a.mediaTypes.types[c])), 
                        f(c, b);
                    }), j.engines[b] = {}, j.engines[b].tpl = d.getEngineTemplate(a), j.engines[b].controller = d.getEngineController(a);
                }
            }, function() {
                g(b);
            });
        });
    };
} ])
.service("Bento",["$routeParams","$rootScope","$q","oneSearch","mediaTypes",
function(a,b,c,d,e){function f(a,b){var c=j.boxes[a].engines.indexOf(b);-1!=
c&&(i(a),j.boxes[a].engines.splice(c,1))}function g(a){angular.forEach(j.boxes,
function(b,c){f(c,a)})}function h(a){var b=j.boxes[a].engines.length,
c=b>1?1:2>b?3:2;j.boxes[a].resultLimit=c}function i(a){c.when(j.boxes[a].results)
.then(function(b){var c=Object.keys(b).length,d=j.boxes[a].engines.length,e=c+d;(2>e&&j.boxes[a].resultLimit<3||3>e&&j.boxes[a].resultLimit<2)&&j.boxes[a]
.resultLimit++})}var j=this;this.boxes={},this.boxMenu=[],this.engines={};
var k;this.getBoxes=function(){k=d.searchAll(a),angular.copy(e.types,j.boxes)
,angular.forEach(j.boxes,function(a,b){h(b),j.boxes[b].results={},j.boxes[b]
.resourceLinks={},j.boxes[b].resourceLinkParams={}}),angular.forEach(k,
function(a,b){a.response.then(function(c){var h=a.getResults(c),i=a.getResourceLink(c);if(isEmpty(h))g(b);
else{h=h.map(function(a,b){var c=a;return c.position=b,c});var k=e.groupBy(h,a.mediaTypes);
Object.keys(j.boxes).forEach(function(c){k.hasOwnProperty(c)&&(j.boxes[c].results[b]=k[c]
.sort(function(a,b){return a.position>b.position?1:a.position<b.position?-1:0}),j.boxes[c].resourceLinks[b]=decodeURIComponent(i[a.id]),angular.isObject
(a.mediaTypes)&&(j.boxes[c].resourceLinkParams[b]=a.mediaTypes.types[c])),
f(c,b)}),j.engines[b]={},j.engines[b].tpl=d.getEngineTemplate(a),j.engines[b]
.controller=d.getEngineController(a)}},function(){g(b)})})}}])

actually one line - no whitespace

Grunt

Configure Tasks

    concat: {
            app: {
                src: ['tmp/templates.js', 'src/app/**/*.js'],
                dest: 'dist/onesearch.js'
            },
            index: {
                src: 'src/index.html',
                dest: 'dist/index.html',
                options: {
                    process: true
                }
            }
        },
        uglify: {
            options: {
                mangle: true
            },
            app: {
                files: {
                    'dist/onesearch.min.js': ['dist/onesearch.js']
                }
            }
        }
/* Dev build task*/
grunt.registerTask('dev', ['concat']);

/* Live build task*/
grunt.registerTask('live', ['dev', 'uglify']);
# Run dev build task
$ grunt dev

# Run live build task
$ grunt live

Grunt

Much, much more

  • Compile
    • LESS
    • SASS
    • Coffee
  • Compress
    • HTML
    • JavaScript
    • CSS
    • Images
  • Clean
  • Copy
  • Release version
  • Shell commands
  • Auto-prefix CSS
  • Run other NodeJS tools
  • Site builders
    • KSS
    • jsDoc
  • Lint
    • HTML
    • JavaScript
    • CSS

Deploy

Manual dev to live flow

  • Manually copy files from dev to live
  • Tempting to just edit live
  • Messier dev gets, the less appealing it is to work from

Deploy

Git Hooks & Live/Dev forks through Github

  • Update Github = update server
  • "Gatekeepers" control flow to live
  • Contributors can make "pull request" when ready to go live
  • "Machine" user owns Live repo
  • Dev repo forks Live

Live/Dev repos

Git hook deployment

Deploy

Git Workflow

All Together

  • Versioned source
  • Automated 
    • dependency management
    • source tasks/builds
    • deployment

Thank You!

University of Alabama Libraries

Will Jones

Senior Web Developer,

University of Alabama Libraries,

Web Infrastructure and Development

Building an Organized, Automated, and Sustainable Workflow Using Bower, Grunt, and Github

By the8bitsquid

Building an Organized, Automated, and Sustainable Workflow Using Bower, Grunt, and Github

  • 5,055