Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial Release

Signed-off-by: Chris Aniszczyk <zx@twitter.com>
  • Loading branch information...
commit c22032933740e6ed4021f798fd579c7d4e961f7a 0 parents
@caniszczyk caniszczyk authored
Showing with 2,102 additions and 0 deletions.
  1. +3 −0  .gitignore
  2. +3 −0  .travis.yml
  3. +7 −0 LICENSE
  4. +4 −0 Makefile
  5. +208 −0 README.md
  6. +40 −0 bin/bower
  7. +35 −0 lib/commands/help.js
  8. +20 −0 lib/commands/index.js
  9. +44 −0 lib/commands/info.js
  10. +47 −0 lib/commands/install.js
  11. +181 −0 lib/commands/list.js
  12. +49 −0 lib/commands/lookup.js
  13. +38 −0 lib/commands/register.js
  14. +49 −0 lib/commands/search.js
  15. +79 −0 lib/commands/uninstall.js
  16. +60 −0 lib/commands/update.js
  17. +4 −0 lib/core/config.js
  18. +150 −0 lib/core/manager.js
  19. +394 −0 lib/core/package.js
  20. +68 −0 lib/core/source.js
  21. +11 −0 lib/index.js
  22. +22 −0 lib/util/hogan-colors.js
  23. +63 −0 lib/util/prune.js
  24. +24 −0 lib/util/read-json.js
  25. +56 −0 lib/util/save.js
  26. +39 −0 lib/util/template.js
  27. +35 −0 package.json
  28. +1 −0  templates/action.mustache
  29. +1 −0  templates/complete.mustache
  30. +1 −0  templates/error.mustache
  31. +8 −0 templates/help-info.mustache
  32. +19 −0 templates/help-install.mustache
  33. +16 −0 templates/help-list.mustache
  34. +8 −0 templates/help-lookup.mustache
  35. +8 −0 templates/help-register.mustache
  36. +9 −0 templates/help-search.mustache
  37. +16 −0 templates/help-uninstall.mustache
  38. +9 −0 templates/help-update.mustache
  39. +16 −0 templates/help.mustache
  40. +6 −0 templates/info.mustache
  41. +1 −0  templates/lookup.mustache
  42. +1 −0  templates/register.mustache
  43. +1 −0  templates/search-empty.mustache
  44. +5 −0 templates/search.mustache
  45. +5 −0 templates/suggestions.mustache
  46. +1 −0  templates/tree-branch.mustache
  47. +1 −0  templates/warn.mustache
  48. +1 −0  templates/warning-missing.mustache
  49. +1 −0  templates/warning-uninstall.mustache
  50. +5 −0 test/assets/package-bootstrap/component.json
  51. +1 −0  test/assets/package-bootstrap/index.js
  52. +6 −0 test/assets/package-jquery/component.json
  53. +1 −0  test/assets/package-jquery/index.js
  54. +1 −0  test/assets/package-no-json/index.js
  55. +5 −0 test/assets/package-other/component.json
  56. +1 −0  test/assets/project/.gitignore
  57. +8 −0 test/assets/project/component.json
  58. +1 −0  test/assets/sprockets/.gitignore
  59. +13 −0 test/assets/sprockets/Rakefile
  60. +3 −0  test/assets/sprockets/assets/application.js
  61. +8 −0 test/assets/sprockets/component.json
  62. +29 −0 test/help.js
  63. +36 −0 test/info.js
  64. +116 −0 test/package.js
3  .gitignore
@@ -0,0 +1,3 @@
+node_modules
+components
+.DS_Store
3  .travis.yml
@@ -0,0 +1,3 @@
+language: node_js
+node_js:
+ - 0.8
7 LICENSE
@@ -0,0 +1,7 @@
+Copyright (c) 2012 Twitter and other contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
4 Makefile
@@ -0,0 +1,4 @@
+test:
+ ./node_modules/.bin/mocha -R spec -t 10000
+
+.PHONY: test
208 README.md
@@ -0,0 +1,208 @@
+BOWER [![Build Status](https://secure.travis-ci.org/twitter/bower.png)](http://travis-ci.org/twitter/bower)
+=====
+
+### Introduction
+
+Bower is a package manager for the web. Bower lets you easily install assets such as images, CSS and JavaScript, and manages dependencies for you.
+
+For example, to install a package, run:
+
+ bower install jquery
+
+This will download jQuery to `./components/jquery`. That's it. The idea is that Bower does package management and package management only.
+
+### Installing Bower
+
+Bower is installed using [Node](http://nodejs.org/) and [npm](http://npmjs.org/) (oh my, how meta).
+
+ npm install bower -g
+
+### Usage
+
+Your best friend at this stage is probably `bower --help`.
+
+To install a package:
+
+ bower install jquery
+ bower install git://github.com/maccman/package-jquery.git
+ bower install http://code.jquery.com/jquery-1.7.2.js
+ bower install ./repos/jquery
+
+As you can see, packages can be installed by name, Git endpoint, URL or local path.
+
+To update a package, reference it by name:
+
+ bower update jquery-ui
+
+To list installed packages:
+
+ bower list
+
+To search for packages:
+
+ bower search [name]
+
+To list all the available packages, just call `bower search` without specifying a name.
+
+### Defining a package
+
+You can create a `component.json` file in your project's root, specifying all of its dependencies. This is similar to Node's `package.json`, or Ruby's `Gemfile`, and is useful for locking down a project dependencies.
+
+```json
+{
+ "name": "myProject",
+ "version": "1.0.0",
+ "main": "./path/to/main.css"
+ "dependencies": {
+ "jquery": "~1.7.2"
+ }
+}
+```
+
+Put this under your project's root, listing all of your dependencies. When you run `bower install`, Bower will read this `component.json` file, resolve all the relevant dependencies and install them.
+
+For now, `name`, `version`, `main`, and `dependencies` are the only properties that are used by Bower. If you have several files you're distributing as part of your package, pass an array to `main` like this:
+
+```json
+{
+ "name": "myProject",
+ "version": "1.0.0",
+ "main": ["./path/to/app.css", "./path/to/app.js", "./path/to/sprite.img"]
+ "dependencies": {
+ "jquery": "~1.7.2"
+ }
+}
+```
+
+### Installing dependencies
+
+Dependencies are installed locally via the `bower install` command. First they’re resolved to find conflicts. Then they’re downloaded and unpacked in a local sub directory called `./components`, for example:
+
+
+```
+/component.json
+/components/jquery/index.js
+/components/jquery/component.json
+```
+
+You can also install packages one at a time `bower install git://my/git/thing`
+
+There are no system wide dependencies, no dependencies are shared between different apps, and the dependency tree is flat.
+
+### Deploying
+
+The easiest approach is to use bower statically, just reference the packages manually from a script tag:
+
+ <script src="components/jquery/index.js"></script>
+
+For more complex projects, you'll probably want to concatenate your scripts. Bower is just a package manager, but there are lots of awesome libraries out there to help you do this, such as [Sprockets](https://github.com/sstephenson/sprockets) and [RequireJS](http://requirejs.org/).
+
+For example, to use Sprockets:
+
+```ruby
+environment = Sprockets::Environment.new
+environment.append_path 'components'
+environment.append_path 'public'
+run environment
+```
+
+### Package Consumption
+
+Bower also makes available a source mapping – this can be used by build tools to easily consume bower components.
+
+If you pass the option `--map` to bower's `list` command it will generate a json with dependency objects. Alternatively, you can pass the `--paths` flag to the `list` command to get a simple path to name mapping:
+
+```json
+{
+ "backbone": "components/backbone/index.js",
+ "jquery": "components/jquery/index.js",
+ "underscore": "components/underscore/index.js"
+}
+```
+
+### Authoring packages
+
+To register a new package, it's as simple as specifying a `component.json`, pushing the package to a Git endpoint, say GitHub, and running:
+
+ bower register myawesomepackagename git://github.com/maccmans/face
+
+There's no authentication or user management. It's on a first come, first served basis. Think of it like a URL shortener. Now anyone can run `bower install myawesomepackagename`, and get your library installed.
+
+### Philosophy
+
+Currently, people are managing dependencies, such as JavaScript libraries, manually. This sucks, and we want to change it.
+
+In a nutshell, Bower is a generic tool which will resolve dependencies and lock packages down to a version. It runs over Git, and is package-agnostic. A package may contain JavaScript, CSS, images, etc., and doesn't rely on any particular transport (AMD, CommonJS, etc).
+
+Bower then makes available a simple programatic api which exposes the package dependency model, so that existing build tools (like Sprockets, LoadBuilder, curls.js, Ender, etc.) can consume it and build files accordingly.
+
+
+### FAQ
+
+**What distinguishes Bower from Jam, Volo or Ender? What does it do better?**
+
+Bower is a lower level component then Jam, Volo, or Ender. These managers could consume bower as a dependency.
+
+Bower's aim is simply to install git paths, resolve dependencies from a component.json, check versions, and then provide an api which reports on these things. Nothing more. This is a major diversion from past attempts at browser package management.
+
+Bower is working under the assumption that there is a single, common problem in frontend application development: dependency resolution. Past attempts (Jam, Volo, Ender) try to tackle this problem in such a way that they actually end up alienating and further segregating the javascript community around transports (sprockets, commonjs, requirejs, regular script tags).
+
+Bower offers a generic, unopionated solution to the problem of package management, while exposing an api that can be consumed by a more opinionated build stack.
+
+**Volo is an arguably more established project and works with the GitHub search API. Will it take long for Bower to contain a decent number of packages?**
+
+Bower (being a git powered package manager) should, in theory, be capable of consuming most every package that Volo does, with the additional benefit of supporting internal networks and other git repositories not hosted on github.
+
+**We recently saw what happened when the NPM registry went down. Is a central point of failure possible for Bower and if so, do you have redundancy planned?**
+
+There's no redundancy planned at the moment, as bower just installs git urls. It's up to the url provider to establish redundancy.
+
+**Isn't having a package.json file going to conflict with my npm's package.json? Will this be a problem?**
+
+Don't use a package.json – user component.json.
+
+**Bower is an open-source Twitter project. How well can we expect it to be maintained in the future?**
+
+Twitter is in the process of migrating it's frontend architecture onto bower, so it's fairly safe to say it will be maintained and invested in going forward.
+
+
+### Contact
+
+Have a question? Ask on our mailing list!
+
+twitter-bower@googlegroups.com
+
+http://groups.google.com/group/twitter-bower
+
+### Authors
+
++ [@fat](http://github.com/fat)
++ [@maccman](http://github.com/maccman)
+
+Thanks for assistance and contributions:
+
++ [@addyosmani](http://github.com/addyosmani)
++ [@angus-c](http://github.com/angus-c)
++ [@borismus](http://github.com/borismus)
++ [@chriseppstein](http://github.com/chriseppstein)
++ [@danwrong](http://github.com/danwrong)
++ [@desandro](http://github.com/desandro)
++ [@isaacs](http://github.com/isaacs)
++ [@josh](http://github.com/josh)
++ [@jrburke](http://github.com/jrburke)
++ [@mklabs](http://github.com/mklabs)
++ [@paulirish](http://github.com/paulirish)
++ [@rvagg](http://github.com/rvagg)
++ [@sindresorhus](http://github.com/sindresorhus)
++ [@SlexAxton](http://github.com/SlexAxton)
++ [@sstephenson](http://github.com/sstephenson)
++ [@tomdale](http://github.com/tomdale)
++ [@visionmedia](http://github.com/visionmedia)
++ [@wagenet](http://github.com/wagenet)
++ [@wycats](http://github.com/wycats)
+
+## License
+
+Copyright 2012 Twitter, Inc.
+
+Licensed under the MIT License
40 bin/bower
@@ -0,0 +1,40 @@
+#!/usr/bin/env node
+
+var semver = require('semver');
+var nopt = require('nopt');
+var path = require('path');
+var pkg = require(path.join(__dirname, '..', 'package.json'));
+
+var template = require('../lib/util/template');
+var bower = require('../lib');
+
+var command;
+var options;
+var shorthand;
+var input = process.argv;
+var cmdList = Object.keys(bower.commands);
+var nodeVer = process.version;
+var reqVer = pkg.engines.node;
+
+process.title = 'bower';
+
+if (reqVer && !semver.satisfies(nodeVer, reqVer)) {
+ throw new Error('Required: node ' + reqVer);
+}
+
+shorthand = { 'v': ['--version'] };
+options = { version: Boolean };
+options = nopt(options, shorthand, process.argv);
+
+bower.version = pkg.version;
+
+if (options.version) return console.log(bower.version);
+if (~cmdList.indexOf(command = options.argv.remain && options.argv.remain.shift())) bower.command = command;
+
+bower.commands[bower.command || 'help'].line(input)
+ .on('data', function (data) { data && console.log(data); })
+ .on('end', function (data) { data && console.log(data); })
+ .on('error', function (err) {
+ if (options.verbose) throw err;
+ else template('error', { message: err.message }).on('data', function (d) { console.log (d); });
+ })
35 lib/commands/help.js
@@ -0,0 +1,35 @@
+// ==========================================
+// BOWER: Help API
+// ==========================================
+// Copyright 2012 Twitter, Inc
+// Licensed under The MIT License
+// http://opensource.org/licenses/MIT
+// ==========================================
+
+var events = require('events');
+var hogan = require('hogan.js');
+var nopt = require('nopt');
+var path = require('path');
+var fs = require('fs');
+var _ = require('underscore');
+
+var template = require('../util/template');
+var config = require('../core/config');
+
+module.exports = function (name) {
+ var context = {};
+ var emitter = new events.EventEmitter;
+ var commands = require('../commands');
+ var templateName = name ? 'help-' + name : 'help';
+
+ if (!name) context = { commands: Object.keys(commands).join(', ') };
+ _.extend(context, config)
+ template(templateName, context).on('data', emitter.emit.bind(emitter, 'end'));
+ return emitter;
+}
+
+module.exports.line = function (argv) {
+ var options = nopt({}, {}, argv);
+ var paths = options.argv.remain.slice(1);
+ return module.exports(paths[0]);
+};
20 lib/commands/index.js
@@ -0,0 +1,20 @@
+// ==========================================
+// BOWER: Public Commands List
+// ==========================================
+// Copyright 2012 Twitter, Inc
+// Licensed under The MIT License
+// http://opensource.org/licenses/MIT
+// ==========================================
+
+module.exports = {
+ 'help': require('./help'),
+ 'install': require('./install'),
+ 'list': require('./list'),
+ 'ls': require('./list'),
+ 'uninstall': require('./uninstall'),
+ 'update': require('./update'),
+ 'lookup': require('./lookup'),
+ 'info': require('./info'),
+ 'register': require('./register'),
+ 'search': require('./search')
+};
44 lib/commands/info.js
@@ -0,0 +1,44 @@
+// ==========================================
+// BOWER: Lookup API
+// ==========================================
+// Copyright 2012 Twitter, Inc
+// Licensed under The MIT License
+// http://opensource.org/licenses/MIT
+// ==========================================
+
+var Emitter = require('events').EventEmitter;
+var nopt = require('nopt');
+
+var template = require('../util/template');
+var source = require('../core/source');
+var help = require('./help');
+
+var optionTypes = { help: Boolean };
+var shorthand = { 'h': ['--help'] };
+
+module.exports = function (name) {
+ var emitter = new Emitter;
+
+ name && source.info(name, function (err, result) {
+ if (err) return emitter.emit('error', err);
+ emitter.emit('end', result);
+ });
+
+ return emitter;
+};
+
+module.exports.line = function (argv) {
+ var emitter = new Emitter;
+ var options = nopt(optionTypes, shorthand, argv);
+ var names = options.argv.remain.slice(1);
+
+ if (options.help || !names.length) return help('info');
+
+ module.exports(names[0])
+ .on('error', emitter.emit.bind(emitter, 'error'))
+ .on('end' , function (data) {
+ template('info', data).on('data', emitter.emit.bind(emitter, 'end'));
+ });
+
+ return emitter;
+};
47 lib/commands/install.js
@@ -0,0 +1,47 @@
+// ==========================================
+// BOWER: Install API
+// ==========================================
+// Copyright 2012 Twitter, Inc
+// Licensed under The MIT License
+// http://opensource.org/licenses/MIT
+// ==========================================
+// 1. Recursively resolve dependencies
+// 2. Intelligently work out which deps to
+// use (versioning)
+// 3. Throw if deps conflict
+// ==========================================
+
+var Emitter = require('events').EventEmitter;
+var async = require('async');
+var nopt = require('nopt');
+
+var Manager = require('../core/manager');
+var save = require('../util/save');
+var list = require('./list');
+var help = require('./help');
+
+var optionTypes = { help: Boolean };
+var shorthand = { 'h': ['--help'], 'S': ['--save'] };
+
+module.exports = function (paths, options) {
+ var emitter = new Emitter;
+ var manager = new Manager(paths)
+
+ if (options && options.save) save(emitter, manager, paths);
+
+ manager
+ .on('data' , emitter.emit.bind(emitter, 'data'))
+ .on('error' , emitter.emit.bind(emitter, 'error'))
+ .on('resolve' , emitter.emit.bind(emitter, 'end'))
+ .resolve();
+
+ return emitter;
+};
+
+module.exports.line = function (argv) {
+ var options = nopt(optionTypes, shorthand, argv);
+ var paths = options.argv.remain.slice(1);
+
+ if (options.help) return help('install');
+ return module.exports(paths, options);
+};
181 lib/commands/list.js
@@ -0,0 +1,181 @@
+// ==========================================
+// BOWER: List API
+// ==========================================
+// Copyright 2012 Twitter, Inc
+// Licensed under The MIT License
+// http://opensource.org/licenses/MIT
+// ==========================================
+
+var Emitter = require('events').EventEmitter;
+var semver = require('semver');
+var archy = require('archy');
+var async = require('async');
+var nopt = require('nopt');
+var path = require('path');
+var _ = require('underscore');
+
+var template = require('../util/template');
+var Manager = require('../core/manager');
+var Package = require('../core/package');
+var config = require('../core/config');
+var help = require('./help');
+
+var shorthand = { 'h': ['--help'] };
+var optionTypes = { help: Boolean, paths: Boolean, map: Boolean };
+
+var getTree = function (packages, subPackages, result) {
+ var result = result || {};
+
+ _.each(subPackages || packages, function (pkg, name) {
+
+ result[pkg.name] = {};
+
+ Object.keys(pkg.json.dependencies || {}).forEach(function (name) {
+ result[pkg.name][name] = {};
+ });
+
+ var subPackages = {};
+
+ Object.keys(pkg.json.dependencies || {}).forEach(function (name) {
+ subPackages[name] = packages[name] || new Package(name, null);
+ });
+
+ getTree(packages, subPackages, result[pkg.name]);
+ });
+
+ return result;
+};
+
+var generatePath = function (name, main) {
+ if (typeof main == 'string') {
+ return path.join(config.directory, name, main);
+ } else {
+ var main = main.map(function (main) { return generatePath(name, main); });
+ return main.length == 1 ? main[0] : main;
+ }
+}
+
+var buildSource = function (pkg, shallow) {
+ var result = {};
+
+ ['main', 'scripts', 'styles', 'templates', 'images'].forEach(function (type) {
+ if (pkg.json[type]) result[type] = generatePath(pkg.name, pkg.json[type]);
+ });
+
+ if (shallow) {
+ result.main = result.main ? result.main
+ : result.scripts ? result.scripts
+ : result.styles ? result.styles
+ : result.templates ? result.templates
+ : result.images ? result.images
+ : generatePath(pkg.name, '');
+ }
+
+ return result;
+}
+
+var shallowTree = function (packages, tree) {
+ var result = {};
+
+ Object.keys(tree).forEach(function (packageName) {
+ result[packageName] = buildSource(packages[packageName], true).main;
+ });
+
+ return result;
+}
+
+var deepTree = function (packages, tree) {
+
+ var result = {};
+
+ Object.keys(tree).forEach(function (packageName) {
+
+ result[packageName] = {};
+ result[packageName].source = buildSource(packages[packageName])
+
+ if (Object.keys(tree[packageName]).length) {
+ result[packageName].dependencies = deepTree(packages, tree[packageName]);
+ }
+
+ });
+
+ return result;
+}
+
+var getNodes = function (packages, tree) {
+ return Object.keys(tree).map(function (key) {
+ var upgrade;
+
+ var version = packages[key].json.repository
+ && packages[key].json.repository.type == 'asset'
+ && packages[key]
+ && packages[key].version == "0.0.0" ?
+ packages[key].json.repository.url : packages[key] && packages[key].version;
+
+ if (version && packages[key].tags.indexOf(version)) {
+ upgrade = packages[key].tags[0];
+ }
+
+ if (Object.keys(tree[key]).length) {
+ return {
+ label: template('tree-branch', { package: key, version: version, upgrade: upgrade }, true),
+ nodes: getNodes(packages, tree[key])
+ };
+ } else {
+ return template('tree-branch', { package: key, version: version, upgrade: upgrade }, true);
+ }
+ });
+}
+
+var cliTree = function (emitter, packages, tree) {
+ emitter.emit('data', archy({
+ label: process.cwd(),
+ nodes: getNodes(packages, tree)
+ }));
+};
+
+module.exports = function (options) {
+ var manager = new Manager;
+ var emitter = new Emitter;
+
+ options = options || {};
+
+ manager.on('data', emitter.emit.bind(emitter, 'data'));
+ manager.on('error', emitter.emit.bind(emitter, 'error'));
+
+ manager.once('resolveLocal', function () {
+ var packages = {};
+
+ Object.keys(manager.dependencies).forEach(function (key) {
+ packages[key] = manager.dependencies[key][0];
+ });
+
+ async.forEach(_.values(packages), function (pkg, next) {
+ pkg.once('loadJSON', function () {
+ if (this.json.repository && this.json.repository.type == 'git') {
+ pkg.once('versions', function (versions) {
+ pkg.tags = versions.map(semver.clean);
+ next();
+ }).versions();
+ } else {
+ pkg.tags = [];
+ next();
+ }
+ }).loadJSON();
+ }, function () {
+ var tree = getTree(packages);
+ if (!options.paths && !options.map && options.argv) return cliTree(emitter, packages, tree);
+ tree = options.paths ? shallowTree(packages, tree) : deepTree(packages, tree);
+ emitter.emit('data', options.argv ? JSON.stringify(tree, null, 2) : tree);
+ });
+
+ }).resolveLocal();
+
+ return emitter;
+};
+
+module.exports.line = function (argv) {
+ var options = nopt(optionTypes, shorthand, argv);
+ if (options.help) return help('list');
+ return module.exports(options);
+};
49 lib/commands/lookup.js
@@ -0,0 +1,49 @@
+// ==========================================
+// BOWER: Lookup API
+// ==========================================
+// Copyright 2012 Twitter, Inc
+// Licensed under The MIT License
+// http://opensource.org/licenses/MIT
+// ==========================================
+
+var Emitter = require('events').EventEmitter;
+var nopt = require('nopt');
+
+var template = require('../util/template');
+var source = require('../core/source');
+var help = require('./help');
+
+var optionTypes = { help: Boolean };
+var shorthand = { 'h': ['--help'] };
+
+module.exports = function (name) {
+ var emitter = new Emitter;
+
+ source.lookup(name, function (err, url) {
+ if (err) {
+ source.search(name, function (err, packages) {
+ if (packages.length) {
+ template('suggestions', {packages: packages, name: name})
+ .on('data', emitter.emit.bind(emitter, 'data'));
+ } else {
+ template('warning-missing', {name: name})
+ .on('data', emitter.emit.bind(emitter, 'data'));
+ }
+ });
+
+ } else {
+ template('lookup', {name: name, url: url})
+ .on('data', emitter.emit.bind(emitter, 'data'));
+ }
+ });
+
+ return emitter;
+};
+
+module.exports.line = function (argv) {
+ var options = nopt(optionTypes, shorthand, argv);
+ var names = options.argv.remain.slice(1);
+
+ if (options.help || !names.length) return help('lookup');
+ return module.exports(names[0]);
+};
38 lib/commands/register.js
@@ -0,0 +1,38 @@
+// ==========================================
+// BOWER: Lookup API
+// ==========================================
+// Copyright 2012 Twitter, Inc
+// Licensed under The MIT License
+// http://opensource.org/licenses/MIT
+// ==========================================
+
+var Emitter = require('events').EventEmitter;
+var nopt = require('nopt');
+
+var template = require('../util/template');
+var source = require('../core/source');
+var help = require('./help');
+
+var optionTypes = { help: Boolean };
+var shorthand = { 'h': ['--help'] };
+
+module.exports = function (name, url) {
+ var emitter = new Emitter;
+
+ source.register(name, url, function (err) {
+ if (err) return emitter.emit('error', err);
+
+ template('register', {name: name, url: url})
+ .on('data', emitter.emit.bind(emitter, 'data'));
+ });
+
+ return emitter;
+};
+
+module.exports.line = function (argv) {
+ var options = nopt(optionTypes, shorthand, argv);
+ var args = options.argv.remain.slice(1);
+
+ if (options.help || args.length != 2) return help('register');
+ return module.exports(args[0], args[1]);
+};
49 lib/commands/search.js
@@ -0,0 +1,49 @@
+// ==========================================
+// BOWER: Lookup API
+// ==========================================
+// Copyright 2012 Twitter, Inc
+// Licensed under The MIT License
+// http://opensource.org/licenses/MIT
+// ==========================================
+
+var Emitter = require('events').EventEmitter;
+var nopt = require('nopt');
+
+var template = require('../util/template');
+var source = require('../core/source');
+var help = require('./help');
+
+var optionTypes = { help: Boolean };
+var shorthand = { 'h': ['--help'] };
+
+module.exports = function (name) {
+ var emitter = new Emitter;
+
+ var callback = function (err, results) {
+ if (err) return emitter.emit('error', err);
+
+ if (results.length) {
+ template('search', {results: results})
+ .on('data', emitter.emit.bind(emitter, 'data'));
+ } else {
+ template('search-empty', {results: results})
+ .on('data', emitter.emit.bind(emitter, 'data'));
+ }
+ };
+
+ if (name) {
+ source.search(name, callback);
+ } else {
+ source.all(callback);
+ }
+
+ return emitter;
+};
+
+module.exports.line = function (argv) {
+ var options = nopt(optionTypes, shorthand, argv);
+ var names = options.argv.remain.slice(1);
+
+ if (options.help) return help('search');
+ return module.exports(names[0]);
+};
79 lib/commands/uninstall.js
@@ -0,0 +1,79 @@
+// ==========================================
+// BOWER: Uninstall API
+// ==========================================
+// Copyright 2012 Twitter, Inc
+// Licensed under The MIT License
+// http://opensource.org/licenses/MIT
+// ==========================================
+
+var Emitter = require('events').EventEmitter;
+var async = require('async');
+var nopt = require('nopt');
+var _ = require('underscore');
+
+var template = require('../util/template');
+var Manager = require('../core/manager');
+var save = require('../util/save');
+var help = require('./help');
+
+var shorthand = { 'h': ['--help'], 'S': ['--save'] };
+var optionTypes = { help: Boolean };
+
+module.exports = function (names, options) {
+
+ var packages, uninstallables;
+ var emitter = new Emitter;
+ var manager = new Manager;
+
+ if (options.save) save.discard(emitter, manager, names);
+
+ manager.on('data', emitter.emit.bind(emitter, 'data'));
+ manager.on('error', emitter.emit.bind(emitter, 'error'));
+
+ var resolveLocal = function () {
+ packages = _.flatten(_.values(manager.dependencies));
+
+ uninstallables = packages.filter(function (pkg) {
+ return _.include(names, pkg.name);
+ });
+
+ async.forEach(packages, function (pkg, next) {
+ pkg.once('loadJSON', next).loadJSON();
+ }, function () {
+ showWarnings();
+ uninstall();
+ });
+ };
+
+ var showWarnings = function () {
+ packages.forEach(function (pkg) {
+ var conflicts = _.intersection(
+ Object.keys(pkg.json.dependencies),
+ _.pluck(uninstallables, 'name')
+ );
+
+ conflicts.forEach(function (conflictName) {
+ template('warning-uninstall', {packageName: pkg.name, conflictName: conflictName})
+ .on('data', emitter.emit.bind(emitter, 'data'));
+ });
+ });
+ };
+
+ var uninstall = function () {
+ async.forEach(uninstallables, function (pkg, next) {
+ pkg.on('uninstall', next).uninstall();
+ }, emitter.emit.bind(emitter, 'end'));
+ };
+
+ manager.on('resolveLocal', resolveLocal).resolveLocal();
+
+ return emitter;
+};
+
+module.exports.line = function (argv) {
+ var options = nopt(optionTypes, shorthand, argv);
+ if (options.help) return help('uninstall');
+ var names = options.argv.remain.slice(1);
+
+ return module.exports(names, options)
+};
60 lib/commands/update.js
@@ -0,0 +1,60 @@
+// ==========================================
+// BOWER: Update API
+// ==========================================
+// Copyright 2012 Twitter, Inc
+// Licensed under The MIT License
+// http://opensource.org/licenses/MIT
+// ==========================================
+
+var Emitter = require('events').EventEmitter;
+var async = require('async');
+var nopt = require('nopt');
+var _ = require('underscore');
+
+var Manager = require('../core/manager');
+var install = require('./install');
+var help = require('./help');
+
+var shorthand = { 'h': ['--help'] };
+var optionTypes = { help: Boolean };
+
+module.exports = function (argv) {
+ var manager = new Manager;
+ var emitter = new Emitter;
+
+ manager.on('data', emitter.emit.bind(emitter, 'data'));
+ manager.on('error', emitter.emit.bind(emitter, 'error'));
+
+ var installURLS = function (err, urls) {
+ var installEmitter = install(urls);
+ installEmitter.on('data', emitter.emit.bind(emitter, 'data'));
+ installEmitter.on('error', emitter.emit.bind(emitter, 'error'));
+ installEmitter.on('end', emitter.emit.bind(emitter, 'end'));
+ };
+
+ manager.once('resolveLocal', function () {
+ var packages = {};
+
+ _.each(manager.dependencies, function (value, name) {
+ packages[name] = value[0];
+ });
+
+ var urls = async.map(_.values(packages), function (pkg, next) {
+ pkg.once('loadJSON', function () {
+ pkg.once('fetchURL', function (url) {
+ next(null, url + '#~' + pkg.version);
+ }).fetchURL();
+ }).loadJSON();
+ }, installURLS);
+ }).resolveLocal();
+
+ return emitter;
+};
+
+module.exports.line = function (argv) {
+ var options = nopt(optionTypes, shorthand, argv);
+ if (options.help) return help('update');
+
+ var paths = options.argv.remain.slice(1);
+ return module.exports(paths);
+};
4 lib/core/config.js
@@ -0,0 +1,4 @@
+module.exports = {
+ directory: 'components',
+ json: 'component.json'
+}
150 lib/core/manager.js
@@ -0,0 +1,150 @@
+// ==========================================
+// BOWER: Manager Object Definition
+// ==========================================
+// Copyright 2012 Twitter, Inc
+// Licensed under The MIT License
+// http://opensource.org/licenses/MIT
+// ==========================================
+// Events:
+// - install: fired when package installed
+// - resolve: fired when deps resolved
+// - error: fired on all errors
+// - data: fired when trying to output data
+// - end: fired when finished installing
+// ==========================================
+
+var Package = require('./package');
+var config = require('./config');
+var prune = require('../util/prune');
+var events = require('events');
+var async = require('async');
+var path = require('path');
+var glob = require('glob');
+var fs = require('fs');
+
+// read local dependencies (with versions)
+// read json dependencies (resolving along the way into temp dir)
+// merge local dependencies with json dependencies
+// prune and move dependencies into local directory
+
+var Manager = function (endpoints) {
+ this.dependencies = {};
+ this.cwd = process.cwd();
+ this.endpoints = endpoints || [];
+};
+
+Manager.prototype = Object.create(events.EventEmitter.prototype);
+Manager.prototype.constructor = Manager;
+
+Manager.prototype.resolve = function () {
+ var resolved = function () {
+ this.prune();
+ this.on('install', this.emit.bind(this, 'resolve'));
+ this.install();
+ }.bind(this);
+
+ this.once('resolveLocal', function () {
+ if (this.endpoints.length) {
+ this.once('resolveEndpoints', resolved).resolveEndpoints();
+ } else {
+ this.once('resolveFromJson', resolved).resolveFromJson();
+ }
+ }).resolveLocal();
+
+ return this;
+};
+
+Manager.prototype.resolveLocal = function () {
+ glob('./' + config.directory + '/*', function (err, dirs) {
+ if (err) return this.emit('error', err);
+ dirs.forEach(function (dir) {
+ var name = path.basename(dir);
+ this.dependencies[name] = [];
+ this.dependencies[name].push(new Package(name, dir, this));
+ }.bind(this));
+ this.emit('resolveLocal');
+ }.bind(this));
+};
+
+Manager.prototype.resolveEndpoints = function () {
+ // Iterate through paths
+ // Add to depedencies array
+ // Prune & install
+
+ async.forEach(this.endpoints, function (endpoint, next) {
+ var name = path.basename(endpoint).replace(/(\.git)?(#.*)?$/, '');
+ var pkg = new Package(name, endpoint, this);
+ this.dependencies[name] = this.dependencies[name] || [];
+ this.dependencies[name].push(pkg);
+ pkg.on('resolve', next).resolve();
+ }.bind(this), this.emit.bind(this, 'resolveEndpoints'));
+};
+
+Manager.prototype.loadJSON = function () {
+ var json = path.join(this.cwd, config.json);
+
+ fs.exists(json, function (exists) {
+ if (!exists) return this.emit('error', new Error('Could not find local ' + config.json));
+ fs.readFile(json, 'utf8', function (err, json) {
+ if (err) return this.emit('error', err);
+ this.json = JSON.parse(json);
+ this.name = this.json.name;
+ this.version = this.json.version;
+ this.emit('loadJSON');
+ }.bind(this));
+ }.bind(this));
+};
+
+Manager.prototype.resolveFromJson = function () {
+ // loadJSON
+ // Resolve dependencies
+ // Add to dependencies array
+ // Prune & install
+
+ this.once('loadJSON', function () {
+
+ async.forEach(Object.keys(this.json.dependencies), function (name, next) {
+ var endpoint = this.json.dependencies[name];
+ var pkg = new Package(name, endpoint, this);
+ this.dependencies[name] = this.dependencies[name] || [];
+ this.dependencies[name].push(pkg);
+ pkg.on('resolve', next).resolve();
+ }.bind(this), this.emit.bind(this, 'resolveFromJson'));
+
+ }.bind(this)).loadJSON();
+};
+
+Manager.prototype.getDeepDependencies = function () {
+ var result = {};
+
+ for (var name in this.dependencies) {
+ this.dependencies[name].forEach(function (pkg) {
+ result[pkg.name] = result[pkg.name] || [];
+ result[pkg.name].push(pkg);
+ pkg.getDeepDependencies().forEach(function (pkg) {
+ result[pkg.name] = result[pkg.name] || [];
+ result[pkg.name].push(pkg);
+ });
+ });
+ }
+
+ return result;
+};
+
+Manager.prototype.prune = function () {
+ try {
+ this.dependencies = prune(this.getDeepDependencies());
+ } catch (err) {
+ this.emit('error', err);
+ }
+ return this;
+};
+
+Manager.prototype.install = function () {
+ async.forEach(Object.keys(this.dependencies), function (name, next) {
+ this.dependencies[name][0].once('install', next).install();
+ }.bind(this), this.emit.bind(this, 'install'));
+ return this;
+};
+
+module.exports = Manager;
394 lib/core/package.js
@@ -0,0 +1,394 @@
+// ==========================================
+// BOWER: Package Object Definition
+// ==========================================
+// Copyright 2012 Twitter, Inc
+// Licensed under The MIT License
+// http://opensource.org/licenses/MIT
+// ==========================================
+// Events:
+// - install: fired when package installed
+// - resolve: fired when deps resolved
+// - error: fired on all errors
+// - data: fired when trying to output data
+// ==========================================
+
+var spawn = require('child_process').spawn;
+var _ = require('underscore');
+var fstream = require('fstream');
+var mkdirp = require('mkdirp');
+var events = require('events');
+var rimraf = require('rimraf');
+var semver = require('semver');
+var async = require('async');
+var https = require('https');
+var http = require('http');
+var path = require('path');
+var url = require('url');
+var tmp = require('tmp');
+var fs = require('fs');
+
+var config = require('./config');
+var source = require('./source');
+var template = require('../util/template');
+var readJSON = require('../util/read-json');
+
+var temp = process.env.TMPDIR
+ || process.env.TMP
+ || process.env.TEMP
+ || process.platform === "win32" ? "c:\\windows\\temp" : "/tmp";
+
+var home = (process.platform === "win32"
+ ? process.env.USERPROFILE
+ : process.env.HOME) || temp;
+
+var cache = process.platform === "win32"
+ ? path.resolve(process.env.APPDATA || home || temp, "bower-cache")
+ : path.resolve(home || temp, ".bower")
+
+var Package = function (name, endpoint, manager) {
+ this.dependencies = {};
+ this.json = {};
+ this.name = name;
+ this.manager = manager;
+
+ if (endpoint) {
+
+ if (/^(.*\.git)$/.exec(endpoint)) {
+ this.gitUrl = RegExp.$1.replace(/^git\+/, '');
+ this.tag = false;
+
+ } else if (/^(.*\.git)#(.*)$/.exec(endpoint)) {
+ this.gitUrl = RegExp.$1.replace(/^git\+/, '');
+ this.tag = RegExp.$2;
+
+ } else if (/^(?:(git):|git\+(https?):)\/\/([^#]+)#?(.*)$/.exec(endpoint)) {
+ this.gitUrl = (RegExp.$1 || RegExp.$2) + "://" + RegExp.$3;
+ this.tag = RegExp.$4;
+
+ } else if (semver.validRange(endpoint)) {
+ this.tag = endpoint;
+
+ } else if (/^[\.\/~]\.?[^.]*\.(js|css)/.test(endpoint)) {
+ this.path = path.resolve(endpoint);
+ this.assetType = path.extname(endpoint);
+ this.name = this.name.replace(this.assetType, '');
+
+ } else if (/^[\.\/~]/.test(endpoint)) {
+ this.path = path.resolve(endpoint);
+
+ } else if (/^https?:\/\//.exec(endpoint)) {
+ this.assetUrl = endpoint;
+ this.assetType = path.extname(endpoint);
+ this.name = this.name.replace(this.assetType, '');
+
+ } else {
+ this.tag = endpoint.split('#', 2)[1];
+ }
+ }
+
+ if (this.manager) {
+ this.on('data', this.manager.emit.bind(this.manager, 'data'));
+ this.on('error', this.manager.emit.bind(this.manager, 'error'));
+ }
+};
+
+Package.prototype = Object.create(events.EventEmitter.prototype);
+
+Package.prototype.constructor = Package;
+
+Package.prototype.resolve = function () {
+
+ if (this.assetUrl) {
+ this.download();
+ } else if (this.gitUrl) {
+ this.clone();
+ } else if (this.path) {
+ this.copy();
+ } else {
+ this.once('lookup', this.clone).lookup();
+ }
+
+ return this;
+};
+
+Package.prototype.lookup = function () {
+ source.lookup(this.name, function (err, url) {
+ if (err) return this.emit('error', err);
+ this.gitUrl = url;
+ this.emit('lookup');
+ }.bind(this));
+};
+
+Package.prototype.install = function () {
+ if (path.resolve(this.path) == this.localPath) return this.emit('install');
+ mkdirp(path.dirname(this.localPath), function (err) {
+ if (err) return this.emit('error', err);
+ rimraf(this.localPath, function (err) {
+ if (err) return this.emit('error', err);
+ return fs.rename(this.path, this.localPath, function () {
+ if (this.gitUrl) this.json.repository = { type: "git", url: this.gitUrl };
+ if (this.assetUrl) this.json = this.generateAssetJSON();
+ fs.writeFile(path.join(this.localPath, config.json), JSON.stringify(this.json, null, 2));
+ rimraf(path.join(this.localPath, '.git'), this.emit.bind(this, 'install'));
+ }.bind(this));
+ }.bind(this));
+ }.bind(this));
+};
+
+Package.prototype.generateAssetJSON = function () {
+ var semverParser = new RegExp('(' + semver.expressions.parse.toString().replace(/\$?\/\^?/g, '') + ')');
+ return {
+ name: this.name,
+ main: 'index' + this.assetType,
+ version: semverParser.exec(this.assetUrl) ? RegExp.$1 : "0.0.0",
+ repository: { type: "asset", url: this.assetUrl }
+ }
+}
+
+Package.prototype.uninstall = function () {
+ template('action', { name: 'uninstalling', shizzle: this.path })
+ .on('data', this.emit.bind(this, 'data'));
+ rimraf(this.path, function (err) {
+ if (err) return this.emit('error', err);
+ this.emit.bind(this, 'uninstall');
+ }.bind(this));
+};
+
+// Private
+Package.prototype.loadJSON = function (name) {
+ var pathname = name || ( this.assetType ? 'index' + this.assetType : config.json );
+ readJSON(path.join(this.path, pathname), function (err, json) {
+ if (err) {
+ if (!name) return this.loadJSON('package.json');
+ return this.assetUrl ? this.emit('loadJSON') : this.path && this.on('describeTag', function (tag) {
+ this.version = this.tag = semver.clean(tag);
+ this.emit('loadJSON')
+ }.bind(this)).describeTag();
+ }
+ this.json = json;
+ this.name = this.json.name;
+ this.version = this.json.version;
+ this.emit('loadJSON');
+ }.bind(this), this);
+}
+
+Package.prototype.download = function () {
+ template('action', { name: 'downloading', shizzle: this.assetUrl })
+ .on('data', this.emit.bind(this, 'data'));
+
+ var file = '';
+ var src = url.parse(this.assetUrl);
+ var req = src.protocol === 'https:' ? https : http;
+
+ tmp.dir(function (err, tmpPath) {
+
+ req.get(src, function (res) {
+
+ res.on('data', function (data) {
+ file += data;
+ });
+
+ res.on('end', function () {
+ fs.writeFile(path.join((this.path = tmpPath), 'index' + this.assetType), file, function () {
+ this.once('loadJSON', this.addDependencies).loadJSON();
+ }.bind(this));
+ }.bind(this));
+
+ }.bind(this)).on('error', this.emit.bind(this, 'error'));
+
+ }.bind(this));
+}
+
+Package.prototype.copy = function () {
+ template('action', { name: 'copying', shizzle: this.path }).on('data', this.emit.bind(this, 'data'));
+
+ tmp.dir(function (err, tmpPath) {
+ fs.stat(this.path, function (err, stats) {
+ if (err) return this.emit('error', err);
+
+ if (this.assetType) {
+ return fs.readFile(this.path, function (err, data) {
+ fs.writeFile(path.join((this.path = tmpPath), 'index' + this.assetType), data, function () {
+ this.once('loadJSON', this.addDependencies).loadJSON();
+ }.bind(this));
+ }.bind(this));
+ }
+
+ var reader = fstream.Reader(this.path).pipe(
+ fstream.Writer({
+ type: 'Directory',
+ path: (this.path = tmpPath)
+ })
+ );
+
+ this.once('loadJSON', this.addDependencies);
+
+ reader.on('error', this.emit.bind(this, 'error'));
+ reader.on('end', this.loadJSON.bind(this));
+ }.bind(this));
+ }.bind(this));
+};
+
+Package.prototype.getDeepDependencies = function (result) {
+ var result = result || [];
+ for (var name in this.dependencies) {
+ result.push(this.dependencies[name])
+ this.dependencies[name].getDeepDependencies(result);
+ }
+ return result;
+};
+
+Package.prototype.addDependencies = function () {
+ var dependencies = this.json.dependencies || {};
+ var callbacks = Object.keys(dependencies).map(function (name) {
+ return function (callback) {
+ var endpoint = dependencies[name];
+ this.dependencies[name] = new Package(name, endpoint, this);
+ this.dependencies[name].once('resolve', callback).resolve();
+ }.bind(this);
+ }.bind(this));
+ async.parallel(callbacks, this.emit.bind(this, 'resolve'));
+};
+
+Package.prototype.exists = function (callback) {
+ fs.exists(this.localPath, callback);
+};
+
+Package.prototype.clone = function () {
+ template('action', { name: 'cloning', shizzle: this.gitUrl }).on('data', this.emit.bind(this, 'data'));
+ this.path = path.resolve(cache, this.name);
+ this.once('cache', function () {
+ this.once('loadJSON', this.copy.bind(this)).checkout();
+ }.bind(this)).cache();
+}
+
+Package.prototype.cache = function () {
+ mkdirp(cache, function (err) {
+ if (err) return this.emit('error', err);
+ fs.stat(this.path, function (err) {
+ if (!err) {
+ template('action', { name: 'cached', shizzle: this.gitUrl }).on('data', this.emit.bind(this, 'data'));
+ return this.emit('cache');
+ }
+ template('action', { name: 'caching', shizzle: this.gitUrl }).on('data', this.emit.bind(this, 'data'));
+ var cp = spawn('git', ['clone', this.gitUrl, this.path]);
+ cp.stderr.setEncoding('utf8');
+ cp.stderr.on('data', this.emit.bind(this, 'data'));
+ cp.on('close', function (code) {
+ if (code != 0) return this.emit('error', new Error('Git status: ' + code));
+ this.emit('cache');
+ }.bind(this));
+ }.bind(this));
+ }.bind(this));
+};
+
+Package.prototype.checkout = function () {
+ template('action', { name: 'fetching', shizzle: this.name })
+ .on('data', this.emit.bind(this, 'data'));
+
+ this.once('versions', function (versions) {
+
+ if (!versions.length) {
+ this.emit('checkout');
+ this.loadJSON();
+ }
+
+ // If tag is specified, try to satisfy it
+ if (this.tag) {
+ versions = versions.filter(function (version) {
+ return semver.satisfies(version, this.tag);
+ }.bind(this));
+
+ if (!versions.length) {
+ return this.emit('error', new Error(
+ 'Can not find tag: ' + this.name + '#' + this.tag
+ ));
+ }
+ }
+
+ // Use latest version
+ this.tag = versions[0];
+
+ if (this.tag) {
+ template('action', {
+ name: 'checking out',
+ shizzle: this.name + '#' + this.tag
+ }).on('data', this.emit.bind(this, 'data'));
+
+ spawn('git', [ 'checkout', '-b', this.tag, this.tag], { cwd: this.path }).on('close', function (code) {
+ if (code == 128) {
+ return spawn('git', [ 'checkout', this.tag], { cwd: this.path }).on('close', function (code) {
+ this.emit('checkout');
+ this.loadJSON();
+ }.bind(this));
+ }
+ if (code != 0) return this.emit('error', new Error('Git status: ' + code));
+ this.emit('checkout');
+ this.loadJSON();
+ }.bind(this));
+ }
+ }).versions();
+};
+
+Package.prototype.describeTag = function () {
+ var cp = spawn('git', ['describe', '--always', '--tag'], { cwd: path.resolve(cache, this.name) });
+
+ var tag = '';
+
+ cp.stdout.setEncoding('utf8');
+ cp.stdout.on('data', function (data) {
+ tag += data;
+ });
+
+ cp.on('close', function (code) {
+ if (code == 128) tag = 'unspecified'.grey; // not a git repo
+ else if (code != 0) return this.emit('error', new Error('Git status: ' + code));
+ this.emit('describeTag', tag.replace(/\n$/, ''));
+ }.bind(this));
+}
+
+Package.prototype.versions = function () {
+ this.on('fetch', function () {
+ var cp = spawn('git', ['tag'], { cwd: path.resolve(cache, this.name) });
+
+ var versions = '';
+
+ cp.stdout.setEncoding('utf8');
+ cp.stdout.on('data', function (data) {
+ versions += data;
+ });
+
+ cp.on('close', function (code) {
+ versions = versions.split("\n");
+ versions = versions.filter(function (ver) {
+ return semver.valid(ver);
+ });
+ versions = versions.sort(function (a, b) {
+ return semver.gt(a, b) ? -1 : 1;
+ });
+ this.emit('versions', versions);
+ }.bind(this));
+ }.bind(this)).fetch();
+};
+
+Package.prototype.fetch = function () {
+ var cp = spawn('git', ['fetch'], { cwd: path.resolve(cache, this.name) });
+ cp.on('close', function (code) {
+ if (code != 0) return this.emit('error', new Error('Git status: ' + code));
+ this.emit('fetch');
+ }.bind(this));
+};
+
+Package.prototype.fetchURL = function () {
+ if (this.json.repository && this.json.repository.type == 'git') {
+ this.emit('fetchURL', this.json.repository.url);
+ } else {
+ this.emit('error', new Error('No git url found for ' + this.json.name));
+ }
+};
+
+Package.prototype.__defineGetter__('localPath', function () {
+ return path.join(process.cwd(), config.directory, this.name);
+});
+
+module.exports = Package;
68 lib/core/source.js
@@ -0,0 +1,68 @@
+// ==========================================
+// BOWER: Source Api
+// ==========================================
+// Copyright 2012 Twitter, Inc
+// Licensed under The MIT License
+// http://opensource.org/licenses/MIT
+// ==========================================
+
+var request = require('request');
+var _ = require('underscore');
+
+var endpoint = 'https://bower.herokuapp.com/packages';
+
+exports.lookup = function (name, callback) {
+ request.get(endpoint + '/' + encodeURIComponent(name), function (err, response, body) {
+ if (err || response.statusCode !== 200) return callback(err || new Error(name + ' not found'));
+ callback(err, body && JSON.parse(body).url);
+ });
+};
+
+exports.register = function (name, url, callback) {
+ var body = {name: name, url: url};
+
+ request.post({url: endpoint, form: body}, function (err, response, body) {
+ if (err) return callback(err);
+
+ if (response.statusCode === 406) {
+ return callback(new Error('Duplicate package'));
+ }
+
+ if (response.statusCode === 400) {
+ return callback(new Error('Incorrect format'));
+ }
+
+ if (response.statusCode !== 201) {
+ return callback(new Error('Unknown error: ' + response.statusCode));
+ }
+
+ callback();
+ });
+};
+
+exports.search = function (name, callback) {
+ request.get(endpoint + '/search/' + encodeURIComponent(name), function (err, response, body) {
+ callback(err, body && JSON.parse(body));
+ });
+};
+
+exports.info = function (name, callback) {
+ exports.lookup(name, function (err, url) {
+ if (err) return callback(err);
+
+ var Package = require('./package');
+ var pkg = new Package(name, url);
+
+ pkg.once('resolve', function () {
+ pkg.once('versions', function (versions) {
+ callback(null, { pkg: pkg, versions: versions });
+ }).versions();
+ }).resolve();
+ });
+};
+
+exports.all = function (callback) {
+ request.get(endpoint, function (err, response, body) {
+ callback(err, body && JSON.parse(body));
+ });
+};
11 lib/index.js
@@ -0,0 +1,11 @@
+// ==========================================
+// BOWER: Public API Defintion
+// ==========================================
+// Copyright 2012 Twitter, Inc
+// Licensed under The MIT License
+// http://opensource.org/licenses/MIT
+// ==========================================
+
+module.exports = {
+ commands: require('./commands')
+};
22 lib/util/hogan-colors.js
@@ -0,0 +1,22 @@
+// ==========================================
+// BOWER: Hogan.js renderWithColors extension
+// ==========================================
+// Copyright 2012 Twitter, Inc
+// Licensed under The MIT License
+// http://opensource.org/licenses/MIT
+// ==========================================
+
+var colors = require('colors');
+var hogan = require('hogan.js');
+var _ = require('underscore');
+
+module.exports = hogan.Template.prototype.renderWithColors = function (context, partials, indent) {
+ context = _.extend({
+ yellow : function (s) { return s.yellow },
+ green : function (s) { return s.green },
+ cyan : function (s) { return s.cyan },
+ grey : function (s) { return s.grey },
+ red : function (s) { return s.red }
+ }, context);
+ return this.ri([context], partials || {}, indent);
+};
63 lib/util/prune.js
@@ -0,0 +1,63 @@
+// ==========================================
+// BOWER: PRUNE
+// ==========================================
+// Copyright 2012 Twitter, Inc
+// Licensed under The MIT License
+// http://opensource.org/licenses/MIT
+// ==========================================
+
+var semver = require('semver');
+var _ = require('underscore');
+
+var versionRequirements = function (dependencyMap) {
+ var result = []
+
+ for (var name in dependencyMap) {
+ dependencyMap[name].forEach(function (pkg) {
+ for (var dep in pkg.json.dependencies) {
+ result[dep] = result[dep] || [];
+ result[dep].concat(pkg.json.dependencies[dep]);
+ }
+ });
+ }
+
+ return result;
+};
+
+var validVersions = function (versions, dependency) {
+ if (!versions || !versions.length) return true;
+
+ if (!semver.valid(dependency.version)) {
+ throw new Error('Invalid semver version "' + dependency.version + '" specified in ' + dependency.name);
+ }
+
+ return _.find(versions, function (version) {
+ return !semver.satisfies(dependency.version, version)
+ });
+};
+
+module.exports = function (dependencyMap) {
+ // generate version requirements
+ // compare dependency map with version requirements
+ // raise exceptions when requirements are not satisified
+ // remove duplicates
+ // select best version
+ // return a pruned dependencyMap
+
+ var result = {};
+ var versionMap = versionRequirements(dependencyMap);
+
+ for (var name in dependencyMap) {
+ dependencyMap[name] = dependencyMap[name]
+ .filter(validVersions.bind(this, versionMap[name]))
+ .sort(function (a, b) { return semver.gt(a.version, b.version) ? -1 : 1; });
+
+ if (!dependencyMap[name].length) {
+ throw new Error('No resolvable dependency for: ' + name);
+ }
+
+ result[name] = [ dependencyMap[name][0] ];
+ }
+
+ return result;
+};
24 lib/util/read-json.js
@@ -0,0 +1,24 @@
+// ==========================================
+// BOWER: read-json.js - with logging fun
+// ==========================================
+// Copyright 2012 Twitter, Inc
+// Licensed under The MIT License
+// http://opensource.org/licenses/MIT
+// ==========================================
+
+var readJSON = require('read-package-json');
+var template = require('./template');
+
+var read = module.exports = function (path, cb, obj) {
+ readJSON.log = {
+ info: function () {},
+ verbose: function () {},
+ warn: function (what, name, shizzle) {
+ if (read.showWarnings) {
+ template('warn', { name: name.replace(/@/, '#'), shizzle: shizzle })
+ .on('data', obj.emit.bind(obj, 'data'));
+ }
+ }
+ }
+ readJSON(path, cb);
+}
56 lib/util/save.js
@@ -0,0 +1,56 @@
+// ==========================================
+// BOWER: SAVE
+// ==========================================
+// Copyright 2012 Twitter, Inc
+// Licensed under The MIT License
+// http://opensource.org/licenses/MIT
+// ==========================================
+
+var path = require('path');
+var fs = require('fs');
+var _ = require('underscore');
+
+var config = require('../core/config');
+
+function save (eventType, modifier, emitter, manager, paths) {
+
+ manager.on(eventType, manager.on('loadJSON', function () {
+
+ if (!this.json) return emitter.emit('error', new Error('Please define a ' + config.json));
+
+ var pkgs = paths.map(function (path) {
+ path = path.split('#')[0];
+
+ return _.find(Object.keys(this.dependencies), function (key) {
+ var dep = this.dependencies[key][0];
+
+ return dep.name == path
+ || (dep.url && dep.url == path)
+ || (dep.path && dep.path == path);
+ }.bind(this));
+
+ }.bind(this));
+ pkgs = _.compact(pkgs).map(function (name) {
+ return this.dependencies[name][0];
+ }.bind(this));
+
+ pkgs.forEach(modifier.bind(this));
+
+ fs.writeFileSync(path.join(this.cwd, config.json), JSON.stringify(this.json, null, 2));
+
+ }).loadJSON.bind(manager));
+
+};
+
+function addDependency(pkg) {
+ var path = (pkg.gitUrl || pkg.assetUrl || pkg.path || '');
+ var tag = pkg.tag ? '#' + pkg.tag : '';
+ this.json.dependencies[pkg.name] = path + tag;
+}
+
+function removeDependency(pkg) {
+ delete this.json.dependencies[pkg.name];
+}
+
+module.exports = save.bind(this, 'resolve', addDependency);
+module.exports.discard = save.bind(this, 'resolveLocal', removeDependency);
39 lib/util/template.js
@@ -0,0 +1,39 @@
+// ==========================================
+// BOWER: Hogan Renderer w/ template cache
+// ==========================================
+// Copyright 2012 Twitter, Inc
+// Licensed under The MIT License
+// http://opensource.org/licenses/MIT
+// ==========================================
+
+var events = require('events');
+var hogan = require('hogan.js');
+var path = require('path');
+var fs = require('fs');
+
+var colors = require('../util/hogan-colors');
+
+var templates = {};
+
+module.exports = function (name, context, sync) {
+ var emitter = new events.EventEmitter;
+
+ var templateName = name + '.mustache';
+ var templatePath = path.join(__dirname, '../../templates/', templateName);
+
+ if (sync) {
+ if (!templates[templatePath]) templates[templatePath] = fs.readFileSync(templatePath, 'utf-8');
+ return hogan.compile(templates[templatePath]).renderWithColors(context);
+ } else if (templates[templatePath]) {
+ process.nextTick(function () {
+ emitter.emit('data', hogan.compile(templates[templatePath]).renderWithColors(context));
+ });
+ } else {
+ fs.readFile(templatePath, 'utf-8', function (err, file) {
+ templates[templatePath] = file;
+ emitter.emit('data', hogan.compile(file).renderWithColors(context));
+ });
+ }
+
+ return emitter;
+};
35 package.json
@@ -0,0 +1,35 @@
+{
+ "name": "bower",
+ "description": "The browser package manager.",
+ "version": "0.1.0",
+ "author": "Twitter",
+ "licenses": [ { "type": "MIT", "url": "http://opensource.org/licenses/MIT" } ],
+ "main": "lib",
+ "homepage": "http://twitter.github.com/bower",
+ "engines": { "node": ">= 0.8.0" },
+ "dependencies": {
+ "tmp" : "latest",
+ "vows" : "latest",
+ "glob" : "latest",
+ "nopt" : "latest",
+ "archy" : "latest",
+ "async" : "latest",
+ "colors" : "latest",
+ "rimraf" : "latest",
+ "mkdirp" : "latest",
+ "semver" : "latest",
+ "request" : "latest",
+ "fstream" : "latest",
+ "hogan.js" : "latest",
+ "underscore" : "latest",
+ "read-package-json" : "latest"
+ },
+ "scripts": {
+ "test": "mocha -R spec -t 10000"
+ },
+ "devDependencies": {
+ "mocha": "latest"
+ },
+ "bin": { "bower": "bin/bower" },
+ "preferGlobal": true
+}
1  templates/action.mustache
@@ -0,0 +1 @@
+bower {{#cyan}}{{name}}{{/cyan}} {{#grey}}{{shizzle}}{{/grey}}
1  templates/complete.mustache
@@ -0,0 +1 @@
+bower {{#cyan}}{{command}}{{/cyan}} {{#grey}}complete{{/grey}}
1  templates/error.mustache
@@ -0,0 +1 @@
+bower {{#red}}error{{/red}} {{#grey}}{{message}}{{/grey}}
8 templates/help-info.mustache
@@ -0,0 +1,8 @@
+
+Usage:
+
+ {{#cyan}}bower{{/cyan}} info {{#grey}}<pkg>{{/grey}}
+
+Description:
+
+ Version info and description of a particular package.
19 templates/help-install.mustache
@@ -0,0 +1,19 @@
+
+Usage:
+
+ {{#cyan}}bower{{/cyan}} install
+ {{#cyan}}bower{{/cyan}} install {{#grey}}<folder>{{/grey}}
+ {{#cyan}}bower{{/cyan}} install {{#grey}}<pkg>{{/grey}}
+ {{#cyan}}bower{{/cyan}} install {{#grey}}<pkg>#<version>{{/grey}}
+
+Options:
+
+ {{#yellow}}--save{{/yellow}} - adds a dependency entry to a project's {{json}}
+
+Can specify one or more:
+
+ {{#cyan}}bower{{/cyan}} install {{#grey}}./foo/package git://example.com#1.0.2{{/grey}}
+
+Description:
+
+ Installs a browser package locally into a {{#grey}}{{directory}}{{/grey}} directory
16 templates/help-list.mustache
@@ -0,0 +1,16 @@
+
+Usage:
+
+ {{#cyan}}bower{{/cyan}} list
+ {{#cyan}}bower{{/cyan}} ls
+
+
+Options:
+
+ {{#yellow}}--map{{/yellow}} - Generates a deep dependency JSON source mapping
+ {{#yellow}}--paths{{/yellow}} - Generates a simple JSON source mapping
+
+
+Description:
+
+ Lists all packages
8 templates/help-lookup.mustache
@@ -0,0 +1,8 @@
+
+Usage:
+
+ {{#cyan}}bower{{/cyan}} lookup {{#grey}}<package>{{/grey}}
+
+Description:
+
+ Looks up a package url by name
8 templates/help-register.mustache
@@ -0,0 +1,8 @@
+
+Usage:
+
+ {{#cyan}}bower{{/cyan}} register {{#cyan}}<name>{{/cyan}} {{#grey}}<url>{{/grey}}
+
+Description:
+
+ Register a package.
9 templates/help-search.mustache
@@ -0,0 +1,9 @@
+
+Usage:
+
+ {{#cyan}}bower{{/cyan}} search
+ {{#cyan}}bower{{/cyan}} search {{#grey}}<name>{{/grey}}
+
+Description:
+
+ Finds all packages or a specific package.
16 templates/help-uninstall.mustache
@@ -0,0 +1,16 @@
+
+Usage:
+
+ {{#cyan}}bower{{/cyan}} uninstall {{#grey}}<package>{{/grey}}
+
+Options:
+
+ {{#yellow}}--save{{/yellow}} - adds a dependency entry to a project's {{json}}
+
+Can specify one or more:
+
+ {{#cyan}}bower{{/cyan}} uninstall {{#grey}}jquery jquery-ui{{/grey}}
+
+Description:
+
+ Uninstalls a browser package locally from your {{#grey}}{{directory}}{{/grey}} directory
9 templates/help-update.mustache
@@ -0,0 +1,9 @@
+
+Usage:
+
+ {{#cyan}}bower{{/cyan}} update
+ {{#cyan}}bower{{/cyan}} update {{#grey}}<package>{{/grey}}
+
+Description:
+
+ Refreshes a package install
16 templates/help.mustache
@@ -0,0 +1,16 @@
+
+Usage:
+
+ {{#cyan}}bower{{/cyan}} <command> {{#grey}}<options>{{/grey}}
+
+Commands:
+
+ {{commands}}
+
+Example:
+
+ {{#grey}}${{/grey}} {{#cyan}}bower{{/cyan}} install {{#grey}}bootstrap spine jquery{{/grey}}
+
+General Help:
+
+ {{#grey}}http://git.io/bower{{/grey}}
6 templates/info.mustache
@@ -0,0 +1,6 @@
+{{#pkg}}{{#cyan}}{{name}}{{/cyan}} {{#grey}}{{url}}{{/grey}}{{/pkg}}
+
+ Versions:
+ {{#versions}}
+ - {{.}}
+ {{/versions}}
1  templates/lookup.mustache
@@ -0,0 +1 @@
+{{#cyan}}{{name}}{{/cyan}} {{#grey}}{{url}}{{/grey}}
1  templates/register.mustache
@@ -0,0 +1 @@
+registered {{#cyan}}{{name}}{{/cyan}} to {{#grey}}{{url}}{{/grey}}
1  templates/search-empty.mustache
@@ -0,0 +1 @@
+{{#yellow}}No results{{/yellow}}
5 templates/search.mustache
@@ -0,0 +1,5 @@
+Search results:
+
+{{#results}}
+ - {{#cyan}}{{name}}{{/cyan}} {{#grey}}{{url}}{{/grey}}
+{{/results}}
5 templates/suggestions.mustache
@@ -0,0 +1,5 @@
+Sorry, {{#red}}{{name}}{{/red}} wasn't found. Did you mean:
+
+{{#packages}}
+ {{#cyan}}{{name}}{{/cyan}} - {{#grey}}{{url}}{{/grey}}
+{{/packages}}
1  templates/tree-branch.mustache
@@ -0,0 +1 @@
+{{#version}}{{#cyan}}{{package}}{{/cyan}}#{{version}}{{/version}}{{^version}}{{#yellow}}{{package}} - not installed{{/yellow}}{{/version}}{{#upgrade}}{{#grey}} ({{upgrade}} now available){{/grey}}{{/upgrade}}
1  templates/warn.mustache
@@ -0,0 +1 @@
+{{#yellow}}warn{{/yellow}} {{name}} {{#grey}}{{shizzle}}{{/grey}}
1  templates/warning-missing.mustache
@@ -0,0 +1 @@
+{{#cyan}}{{name}}{{/cyan}} {{#yellow}}was not found{{/yellow}}
1  templates/warning-uninstall.mustache
@@ -0,0 +1 @@
+{{#yellow}}warning{{/yellow}} {{#cyan}}{{packageName}}{{/cyan}} depends on {{#grey}}{{conflictName}}{{/grey}}
5 test/assets/package-bootstrap/component.json
@@ -0,0 +1,5 @@
+{
+ "name": "bootstrap",
+ "version": "1.0.0",
+ "dependencies": {}
+}
1  test/assets/package-bootstrap/index.js
@@ -0,0 +1 @@
+// Here be jQuery
6 test/assets/package-jquery/component.json
@@ -0,0 +1,6 @@
+{
+ "name": "jquery",
+ "version": "1.0.0",
+ "main": ["index.js"],
+ "dependencies": {}
+}
1  test/assets/package-jquery/index.js
@@ -0,0 +1 @@
+var x = 'foo';
1  test/assets/package-no-json/index.js
@@ -0,0 +1 @@
+// Here be jQuery
5 test/assets/package-other/component.json
@@ -0,0 +1,5 @@
+{
+ "name": "other",
+ "version": "1.0.0",
+ "dependencies": {}
+}
1  test/assets/project/.gitignore
@@ -0,0 +1 @@
+browser_modules
8 test/assets/project/component.json
@@ -0,0 +1,8 @@
+{
+ "name": "myproject",
+ "version": "1.0.0",
+ "dependencies": {
+ "package-bootstrap": "git://github.com/fat/package-bootstrap.git#~2.0.0",
+ "jquery": "git://github.com/maccman/package-jquery.git#v1.7.2"
+ }
+}
1  test/assets/sprockets/.gitignore
@@ -0,0 +1 @@
+public/application.js
13 test/assets/sprockets/Rakefile
@@ -0,0 +1,13 @@
+task :build do
+ require 'sprockets'
+
+ environment = Sprockets::Environment.new
+ environment.append_path('browser_modules')
+ environment.append_path('assets')
+
+ File.open('public/application.js', 'w+') do |f|
+ f.write environment['application.js'].to_s
+ end
+end
+
+task :default => :build
3  test/assets/sprockets/assets/application.js
@@ -0,0 +1,3 @@
+//= require jquery
+//= require jquery-ui
+// My app
8 test/assets/sprockets/component.json
@@ -0,0 +1,8 @@
+{
+ "name": "myproject",
+ "version": "1.0.0",
+ "dependencies": {
+ "package-bootstrap": "git://github.com/maccman/package-bootstrap.git#~1.0.0",
+ "jquery-ui": "latest"
+ }
+}
29 test/help.js
@@ -0,0 +1,29 @@
+var assert = require('assert');
+var events = require('events');
+var help = require('../lib/commands/help');
+
+describe('help', function () {
+
+ it('Should have line method', function () {
+ assert(!!help.line);
+ });
+
+ it('Should return an emiter', function () {
+ assert(help() instanceof events.EventEmitter);
+ });
+
+ it('Should emit end event', function (next) {
+ help('install').on('end', function (data) {
+ assert(!!data);
+ next();
+ });
+ });
+
+ it('Should emit end event with data string', function (next) {
+ help('install').on('end', function (data) {
+ assert(typeof data == 'string');
+ next();
+ });
+ });
+
+});
36 test/info.js
@@ -0,0 +1,36 @@
+var assert = require('assert');
+var events = require('events');
+var info = require('../lib/commands/info');
+
+describe('info', function () {
+
+ it('Should have line method', function () {
+ assert(!!info.line);
+ });
+
+ it('Should return an emiter', function () {
+ assert(info() instanceof events.EventEmitter);
+ });
+
+ it('Should emit end event', function (next) {
+ info('jquery').on('end', function (data) {
+ assert(!!data);
+ next();
+ });
+ });
+
+ it('Should emit end event with data.pkg object', function (next) {
+ info('jquery').on('end', function (data) {
+ assert(typeof data.pkg == 'object');
+ next();
+ });
+ });
+
+ it('Should emit end event with data.versions array', function (next) {
+ info('jquery').on('end', function (data) {
+ assert(typeof data.versions == 'object');
+ next();
+ });
+ });
+
+});
116 test/package.js
@@ -0,0 +1,116 @@
+var assert = require('assert');
+var path = require('path');
+var fs = require('fs');
+var _ = require('underscore');
+var Package = require('../lib/core/package');
+
+describe('package', function () {
+ it('Should resolve git URLs properly', function () {
+ var pkg = new Package('jquery', 'git://github.com/jquery/jquery.git');
+ assert.equal(pkg.gitUrl, 'git://github.com/jquery/jquery.git');
+ });
+
+ it('Should resolve git HTTP URLs properly', function () {
+ var pkg = new Package('jquery', 'git+http://example.com/project.git');
+ assert.equal(pkg.gitUrl, 'http://example.com/project.git');
+ });
+
+ it('Should resolve git HTTPS URLs properly', function () {
+ var pkg = new Package('jquery', 'git+https://example.com/project.git');
+ assert.equal(pkg.gitUrl, 'https://example.com/project.git');
+ });
+
+ it('Should resolve git URL tags', function () {
+ var pkg = new Package('jquery', 'git://github.com/jquery/jquery.git#v1.0.1');
+ assert.equal(pkg.tag, 'v1.0.1');
+ });
+
+ it('Should resolve github urls', function () {
+ var pkg = new Package('jquery', 'git@github.com:twitter/flight.git#v1.0.1');
+ assert.equal(pkg.tag, 'v1.0.1');
+ assert.equal(pkg.gitUrl, 'git@github.com:twitter/flight.git');
+ });
+
+ it('Should resolve paths properly', function () {
+ var pkg = new Package('jquery', '~/jquery');
+ assert.equal(pkg.path, path.resolve('~/jquery'));
+ });
+
+ it('Should clone git packages', function (next) {
+ var pkg = new Package('jquery', 'git://github.com/maccman/package-jquery.git');
+
+ pkg.on('resolve', function () {
+ assert(pkg.path);
+ assert(fs.existsSync(pkg.path));
+ next();
+ });
+
+ pkg.on('error', function (err) {
+ throw new Error(err);
+ });
+
+ pkg.clone();
+ });
+
+ it('Should copy path packages', function (next) {
+ var pkg = new Package('jquery', __dirname + '/assets/package-jquery');
+
+ pkg.on('resolve', function () {
+ assert(pkg.path);
+ assert(fs.existsSync(pkg.path));
+ next();
+ });
+
+ pkg.on('error', function (err) {
+ throw new Error(err);
+ });
+
+ pkg.copy();
+ });
+
+ it('Should error on clone fail', function (next) {
+ var pkg = new Package('random', 'git://example.com');
+
+ pkg.on('error', function (err) {