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
- Distributed versioning system
- Commonly used
- Flexible repository structure
- Content VCS - only content stored
- 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
- Single origin
- Single branch
- 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
- Website: http://www.lib.ua.edu
- Github: http://github.com/ualibweb
Will Jones
Senior Web Developer,
University of Alabama Libraries,
Web Infrastructure and Development
- Github: http://github.com/8bitsquid
Tools I like
- AngularJS: https://angularjs.org/
- NodeJS: https://nodejs.org
- Grunt: http://gruntjs.com/
- Bower: http://bower.io
- PhpStorm: https://www.jetbrains.com/phpstorm
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,091