├── .gitattributes
├── client
├── favicon.ico
├── apple-touch-icon.png
├── .jshintrc
├── assets
│ ├── scripts
│ │ ├── filters
│ │ │ └── from-now.js
│ │ ├── directives
│ │ │ ├── popover.js
│ │ │ ├── console.js
│ │ │ ├── navigation.js
│ │ │ ├── tooltip.js
│ │ │ ├── new-tab.js
│ │ │ ├── pagination.js
│ │ │ ├── scroll.js
│ │ │ └── focus.js
│ │ ├── controllers
│ │ │ ├── home.js
│ │ │ ├── settings.js
│ │ │ ├── navigation.js
│ │ │ ├── console.js
│ │ │ ├── search.js
│ │ │ ├── pagination.js
│ │ │ └── search-results.js
│ │ ├── values
│ │ │ ├── whitelist.js
│ │ │ └── ignore.js
│ │ ├── services
│ │ │ ├── socket.js
│ │ │ ├── process.js
│ │ │ ├── settings.js
│ │ │ ├── bower.js
│ │ │ └── search.js
│ │ ├── config.js
│ │ └── app.js
│ ├── templates
│ │ ├── search.html
│ │ ├── console.html
│ │ ├── pagination.html
│ │ ├── navigation.html
│ │ ├── home.html
│ │ ├── settings.html
│ │ └── search-results.html
│ └── styles
│ │ └── app.scss
└── index.html
├── resources
├── features.png
└── screenshot.png
├── .gitignore
├── .jshintrc
├── .travis.yml
├── test
├── server.js
├── fixtures
│ └── bower.json
└── server-test.js
├── .editorconfig
├── lib
├── utils
│ ├── is-json-file.js
│ ├── is-new-file.js
│ └── verify-env.js
├── cli.js
└── index.js
├── .jscsrc
├── bin
└── bower-browser
├── LICENSE
├── CHANGELOG.md
├── package.json
├── README.md
└── gulpfile.js
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
2 |
--------------------------------------------------------------------------------
/client/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rakuten-frontend/bower-browser/HEAD/client/favicon.ico
--------------------------------------------------------------------------------
/resources/features.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rakuten-frontend/bower-browser/HEAD/resources/features.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.log
2 | *.map
3 | .DS_Store
4 | node_modules/
5 | bower_components/
6 | .sass-cache/
7 | /lib/public/
8 |
--------------------------------------------------------------------------------
/resources/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rakuten-frontend/bower-browser/HEAD/resources/screenshot.png
--------------------------------------------------------------------------------
/client/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rakuten-frontend/bower-browser/HEAD/client/apple-touch-icon.png
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "eqeqeq": true,
3 | "latedef": true,
4 | "noarg": true,
5 | "undef": true,
6 | "unused": true,
7 | "strict": true,
8 | "node": true
9 | }
10 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | sudo: false
3 | node_js:
4 | - "stable"
5 | - "0.12"
6 | - "0.10"
7 | before_install:
8 | - npm install -g bower
9 | - gem install sass
10 |
--------------------------------------------------------------------------------
/test/server.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var bowerBrowser = require('../lib/');
4 |
5 | bowerBrowser({
6 | path: __dirname + '/fixtures',
7 | port: 3100,
8 | open: false
9 | });
10 |
--------------------------------------------------------------------------------
/client/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "eqeqeq": true,
3 | "latedef": true,
4 | "noarg": true,
5 | "undef": true,
6 | "unused": true,
7 | "strict": true,
8 | "browser": true,
9 | "node": true
10 | }
11 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
13 |
--------------------------------------------------------------------------------
/client/assets/scripts/filters/from-now.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var moment = require('moment');
4 |
5 | module.exports = [
6 | function () {
7 |
8 | return function (date) {
9 | return moment(date).fromNow();
10 | };
11 |
12 | }
13 | ];
14 |
--------------------------------------------------------------------------------
/lib/utils/is-json-file.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var fs = require('fs');
4 |
5 | module.exports = function (filepath) {
6 |
7 | try {
8 | var data = fs.readFileSync(filepath);
9 | JSON.parse(data);
10 | }
11 | catch (e) {
12 | return false;
13 | }
14 | return true;
15 |
16 | };
17 |
--------------------------------------------------------------------------------
/client/assets/scripts/directives/popover.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var $ = require('jquery');
4 |
5 | module.exports = [
6 | function () {
7 |
8 | return {
9 | restrict: 'A',
10 | link: function (scope, element) {
11 | $(element).popover();
12 | }
13 | };
14 |
15 | }
16 | ];
17 |
--------------------------------------------------------------------------------
/client/assets/scripts/controllers/home.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = [
4 | '$scope',
5 | 'BowerService',
6 | 'ProcessService',
7 | function ($scope, BowerService, ProcessService) {
8 |
9 | // Properties
10 | $scope.bower = BowerService;
11 | $scope.process = ProcessService;
12 |
13 | }
14 | ];
15 |
--------------------------------------------------------------------------------
/lib/utils/is-new-file.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var fs = require('fs');
4 |
5 | module.exports = function (filepath, tty) {
6 |
7 | try {
8 | var stat = fs.statSync(filepath);
9 | var isNew = new Date() - stat.mtime < tty * 1000;
10 | }
11 | catch (e) {
12 | return false;
13 | }
14 | return isNew;
15 |
16 | };
17 |
--------------------------------------------------------------------------------
/test/fixtures/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "test-project",
3 | "version": "0.0.0",
4 | "dependencies": {
5 | "angular": "~1.3.14",
6 | "bootstrap": "~3.3.2",
7 | "jquery": "~2.1.3",
8 | "lodash": "~3.3.1",
9 | "moment": "~2.9.0"
10 | },
11 | "devDependencies": {
12 | "chai": "~2.1.0",
13 | "mocha": "~2.1.0"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/client/assets/templates/search.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/client/assets/scripts/directives/console.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var fs = require('fs');
4 |
5 | module.exports = [
6 | function () {
7 |
8 | return {
9 | restrict: 'EA',
10 | replace: true,
11 | scope: true,
12 | controller: 'ConsoleController',
13 | template: fs.readFileSync(__dirname + '/../../templates/console.html', 'utf8')
14 | };
15 |
16 | }
17 | ];
18 |
--------------------------------------------------------------------------------
/client/assets/scripts/directives/navigation.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var fs = require('fs');
4 |
5 | module.exports = [
6 | function () {
7 |
8 | return {
9 | restrict: 'EA',
10 | replace: true,
11 | scope: true,
12 | controller: 'NavigationController',
13 | template: fs.readFileSync(__dirname + '/../../templates/navigation.html', 'utf8')
14 | };
15 |
16 | }
17 | ];
18 |
--------------------------------------------------------------------------------
/client/assets/scripts/directives/tooltip.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var $ = require('jquery');
4 |
5 | module.exports = [
6 | function () {
7 |
8 | return {
9 | restrict: 'A',
10 | link: function (scope, element) {
11 | var $element = $(element);
12 | $element.tooltip();
13 | $element.click(function () {
14 | $element.tooltip('hide');
15 | });
16 | }
17 | };
18 |
19 | }
20 | ];
21 |
--------------------------------------------------------------------------------
/client/assets/scripts/directives/new-tab.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = [
4 | function () {
5 |
6 | return {
7 | restrict: 'A',
8 | link: function (scope, element, attrs) {
9 | scope.$watch(attrs.appNewTab, function (newTab) {
10 | if (newTab) {
11 | element.attr('target', '_blank');
12 | return;
13 | }
14 | element.removeAttr('target');
15 | });
16 | }
17 | };
18 |
19 | }
20 | ];
21 |
--------------------------------------------------------------------------------
/client/assets/scripts/directives/pagination.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var fs = require('fs');
4 |
5 | module.exports = [
6 | function () {
7 |
8 | return {
9 | restrict: 'EA',
10 | replace: true,
11 | controller: 'PaginationController',
12 | template: fs.readFileSync(__dirname + '/../../templates/pagination.html', 'utf8'),
13 | scope: {
14 | min: '=?',
15 | max: '=',
16 | current: '=',
17 | offset: '=?'
18 | }
19 | };
20 |
21 | }
22 | ];
23 |
--------------------------------------------------------------------------------
/client/assets/scripts/directives/scroll.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = [
4 | '$timeout',
5 | function ($timeout) {
6 |
7 | return {
8 | restrict: 'A',
9 | link: function (scope, element, attrs) {
10 | // Scroll to bottom when model is changed
11 | scope.$watch(attrs.appScroll, function () {
12 | $timeout(function () {
13 | element.duScrollTop(element.prop('scrollHeight') - element.prop('offsetHeight'), 150);
14 | });
15 | });
16 | }
17 | };
18 |
19 | }
20 | ];
21 |
--------------------------------------------------------------------------------
/client/assets/templates/console.html:
--------------------------------------------------------------------------------
1 |
14 |
--------------------------------------------------------------------------------
/lib/cli.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var updateNotifier = require('update-notifier');
4 | var bowerBrowser = require('./');
5 | var pkg = require('../package.json');
6 |
7 | module.exports = function (program) {
8 |
9 | var options = {
10 | path: program.path,
11 | port: program.port,
12 | cache: program.cache,
13 | open: !program.skipOpen,
14 | silent: !!program.silent
15 | };
16 |
17 | var notifier = updateNotifier({pkg: pkg});
18 | if (notifier.update && !options.silent) {
19 | notifier.notify();
20 | }
21 |
22 | bowerBrowser(options);
23 |
24 | };
25 |
--------------------------------------------------------------------------------
/client/assets/scripts/controllers/settings.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var _ = require('lodash');
4 |
5 | module.exports = [
6 | '$scope',
7 | 'SettingsService',
8 | function ($scope, SettingsService) {
9 |
10 | // Properties
11 | $scope.settings = SettingsService;
12 | $scope.config = SettingsService.config;
13 |
14 | // Save settings when updated
15 | $scope.$watch('config', function (newValue, oldValue) {
16 | if ($scope.settings.loaded && !_.isEqual(newValue, oldValue)) {
17 | $scope.settings.save();
18 | }
19 | }, true);
20 |
21 | }
22 | ];
23 |
--------------------------------------------------------------------------------
/.jscsrc:
--------------------------------------------------------------------------------
1 | {
2 | "preset": "google",
3 | "requireParenthesesAroundIIFE": true,
4 | "requireSpacesInFunctionExpression": {
5 | "beforeOpeningRoundBrace": true,
6 | "beforeOpeningCurlyBrace": true
7 | },
8 | "requireSpacesInAnonymousFunctionExpression": {
9 | "beforeOpeningRoundBrace": true,
10 | "beforeOpeningCurlyBrace": true
11 | },
12 | "disallowSpacesInAnonymousFunctionExpression": null,
13 | "disallowKeywordsOnNewLine": [],
14 | "maximumLineLength": null,
15 | "requireCapitalizedConstructors": true,
16 | "requireDotNotation": true,
17 | "validateLineBreaks": "LF"
18 | }
19 |
--------------------------------------------------------------------------------
/client/assets/scripts/directives/focus.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = [
4 | '$timeout',
5 | '$parse',
6 | function ($timeout, $parse) {
7 |
8 | return {
9 | restrict: 'A',
10 | link: function (scope, element, attrs) {
11 | var model = $parse(attrs.appFocus);
12 | scope.$watch(model, function (val) {
13 | if (val) {
14 | $timeout(function () {
15 | element[0].focus();
16 | });
17 | }
18 | });
19 | element.bind('blur', function () {
20 | scope.$apply(model.assign(scope, false));
21 | });
22 | }
23 | };
24 |
25 | }
26 | ];
27 |
--------------------------------------------------------------------------------
/client/assets/scripts/controllers/navigation.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = [
4 | '$scope',
5 | '$rootScope',
6 | 'SettingsService',
7 | function ($scope, $rootScope, SettingsService) {
8 |
9 | // Properties
10 | $scope.config = SettingsService.config;
11 | $scope.shown = false;
12 |
13 | // Show navigation (for tablet layout)
14 | $scope.show = function () {
15 | $scope.shown = true;
16 | };
17 |
18 | // Hide navigation (for tablet layout)
19 | $scope.hide = function () {
20 | $scope.shown = false;
21 | };
22 |
23 | $rootScope.$on('$stateChangeStart', function () {
24 | $scope.hide();
25 | });
26 |
27 | }
28 | ];
29 |
--------------------------------------------------------------------------------
/bin/bower-browser:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | 'use strict';
3 |
4 | var program = require('commander');
5 | var cli = require('../lib/cli');
6 | var pkg = require('../package.json');
7 |
8 | program
9 | .version(pkg.version)
10 | .usage('[options]')
11 | .option('--path ', 'location of bower.json (default: use process.cwd())')
12 | .option('--port ', 'port number of bower-browser server (default: 3010)', parseInt)
13 | .option('--cache ', 'cache TTL for package list API (default: 86400 = 24hours)', parseInt)
14 | .option('--skip-open', 'prevent opening web browser at the start')
15 | .option('--silent', 'print nothing to stdout')
16 | .parse(process.argv);
17 |
18 | cli(program);
19 |
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | bower-browser
6 |
7 |
8 |
9 |
10 |
11 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/client/assets/scripts/values/whitelist.js:
--------------------------------------------------------------------------------
1 | /**
2 | * List of certain entries
3 | * (c) The Bower team
4 | * https://github.com/bower/search/blob/gh-pages/js/config/whitelist.js
5 | */
6 |
7 | 'use strict';
8 |
9 | // Limit certain packages to just one name/entry only
10 | // URL => packageName
11 | module.exports = {
12 | 'https://github.com/angular/bower-angular': 'angular',
13 | 'https://github.com/twbs/bootstrap': 'bootstrap',
14 | 'https://github.com/FortAwesome/Font-Awesome': 'fontawesome',
15 | 'https://github.com/jquery/jquery': 'jquery',
16 | 'https://github.com/jabranr/Socialmedia': 'socialmedia',
17 | 'https://github.com/jashkenas/underscore': 'underscore',
18 | 'https://github.com/moment/moment': 'moment',
19 | 'https://github.com/Knockout-Contrib/Knockout-Validation': 'knockout-validation'
20 | };
21 |
--------------------------------------------------------------------------------
/lib/utils/verify-env.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var which = require('which');
4 | var isRoot = require('is-root');
5 | var chalk = require('chalk');
6 |
7 | module.exports = function () {
8 |
9 | var warnings = [];
10 |
11 | // Require bower installed
12 | try {
13 | which.sync('bower');
14 | }
15 | catch (e) {
16 | warnings.push(chalk.red('"bower" not found!') + '\nbower-browser executes "bower" in background.\nPlease install "bower" and run bower-browser again.\n$ npm install -g bower\n');
17 | }
18 |
19 | // Prevent running with `sudo`
20 | if (isRoot()) {
21 | warnings.push(chalk.red('Failed to start bower-browser!') + '\nbower-browser doesn\'t support running with root privileges\nbecause "bower" is a user command.\nTry running bower-browser without "sudo".\n');
22 | }
23 |
24 | return warnings.length ? warnings : null;
25 |
26 | };
27 |
--------------------------------------------------------------------------------
/client/assets/scripts/services/socket.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var io = require('socket.io-client');
4 |
5 | module.exports = [
6 | '$rootScope',
7 | function ($rootScope) {
8 |
9 | var socket = io();
10 | var service = {
11 |
12 | // WebSocket receiver
13 | on: function (eventName, callback) {
14 | socket.on(eventName, function () {
15 | var args = arguments;
16 | $rootScope.$apply(function () {
17 | callback.apply(socket, args);
18 | });
19 | });
20 | },
21 |
22 | // WebSocket sender
23 | emit: function (eventName, data, callback) {
24 | socket.emit(eventName, data, function () {
25 | var args = arguments;
26 | $rootScope.$apply(function () {
27 | if (callback) {
28 | callback.apply(socket, args);
29 | }
30 | });
31 | });
32 | }
33 |
34 | };
35 |
36 | return service;
37 |
38 | }
39 | ];
40 |
--------------------------------------------------------------------------------
/client/assets/templates/pagination.html:
--------------------------------------------------------------------------------
1 |
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014-2015 Rakuten, Inc.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/client/assets/scripts/controllers/console.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = [
4 | '$scope',
5 | '$timeout',
6 | 'ProcessService',
7 | function ($scope, $timeout, ProcessService) {
8 |
9 | // Properties
10 | $scope.templateUrl = '/assets/templates/console.html';
11 | $scope.process = ProcessService;
12 | $scope.shown = false;
13 | $scope.forceShown = false;
14 |
15 | // Show panel
16 | $scope.show = function (force) {
17 | $scope.shown = true;
18 | if (force) {
19 | $scope.forceShown = true;
20 | }
21 | };
22 |
23 | // Hide panel
24 | $scope.hide = function (force) {
25 | if (force) {
26 | $scope.shown = false;
27 | $scope.forceShown = false;
28 | }
29 | else if (!$scope.forceShown) {
30 | $scope.shown = false;
31 | }
32 | };
33 |
34 | // Update log message
35 | $scope.$watch('process.running', function (running) {
36 | if (running) {
37 | $scope.show();
38 | }
39 | else {
40 | $timeout(function () {
41 | $scope.hide();
42 | }, 1000);
43 | }
44 | });
45 |
46 | }
47 | ];
48 |
--------------------------------------------------------------------------------
/client/assets/scripts/controllers/search.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = [
4 | '$scope',
5 | '$state',
6 | 'SearchService',
7 | function ($scope, $state, SearchService) {
8 |
9 | // Properties
10 | $scope.service = SearchService;
11 | $scope.query = $state.params.q || '';
12 | $scope.focus = false;
13 |
14 | // Auto focus on the input field
15 | $scope.handleFocus = function () {
16 | if ($scope.service.loaded) {
17 | $scope.focus = true;
18 | }
19 | else {
20 | $scope.focus = false;
21 | }
22 | };
23 |
24 | // Incremental search
25 | $scope.$watch('query', function (newValue, oldValue) {
26 | if (newValue !== oldValue) {
27 | $state.go('search.results', {q: newValue, p: null});
28 | }
29 | });
30 |
31 | // Sync input value with query param
32 | $scope.$on('$stateChangeSuccess', function (event, state, params) {
33 | $scope.query = params.q || '';
34 | });
35 |
36 | // Events for auto focus
37 | $scope.$watch('service.loaded', function () {
38 | $scope.handleFocus();
39 | });
40 | $scope.$on('$stateChangeSuccess', function () {
41 | $scope.handleFocus();
42 | });
43 |
44 | }
45 | ];
46 |
--------------------------------------------------------------------------------
/client/assets/templates/navigation.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
×
6 |
bower-browser
7 |
18 |
29 |
30 |
--------------------------------------------------------------------------------
/client/assets/scripts/config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var fs = require('fs');
4 |
5 | module.exports = [
6 | '$stateProvider',
7 | '$urlRouterProvider',
8 | '$locationProvider',
9 | '$uiViewScrollProvider',
10 | function ($stateProvider, $urlRouterProvider, $locationProvider, $uiViewScrollProvider) {
11 |
12 | // Redirects
13 | $urlRouterProvider.when('/search', '/search/');
14 | $urlRouterProvider.otherwise('/');
15 |
16 | // Routes
17 | $stateProvider
18 | .state('home', {
19 | url: '/',
20 | template: fs.readFileSync(__dirname + '/../templates/home.html', 'utf8'),
21 | controller: 'HomeController'
22 | })
23 | .state('search', {
24 | url: '/search',
25 | template: fs.readFileSync(__dirname + '/../templates/search.html', 'utf8'),
26 | controller: 'SearchController'
27 | })
28 | .state('search.results', {
29 | url: '/?q&p&s&o',
30 | template: fs.readFileSync(__dirname + '/../templates/search-results.html', 'utf8'),
31 | controller: 'SearchResultsController'
32 | })
33 | .state('settings', {
34 | url: '/settings',
35 | template: fs.readFileSync(__dirname + '/../templates/settings.html', 'utf8'),
36 | controller: 'SettingsController'
37 | });
38 |
39 | // Use # url
40 | $locationProvider.html5Mode(false);
41 |
42 | // Use $anchorScroll behavior for ui-view
43 | $uiViewScrollProvider.useAnchorScroll();
44 |
45 | }
46 | ];
47 |
--------------------------------------------------------------------------------
/client/assets/scripts/app.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var angular = require('angular');
4 |
5 | require('bootstrap-sass');
6 |
7 | angular
8 | .module('bowerBrowser', [
9 | require('angular-ui-router'),
10 | require('angular-scroll')
11 | ])
12 | .config(require('./config'))
13 | .controller('HomeController', require('./controllers/home'))
14 | .controller('SearchController', require('./controllers/search'))
15 | .controller('SearchResultsController', require('./controllers/search-results'))
16 | .controller('SettingsController', require('./controllers/settings'))
17 | .controller('NavigationController', require('./controllers/navigation'))
18 | .controller('ConsoleController', require('./controllers/console'))
19 | .controller('PaginationController', require('./controllers/pagination'))
20 | .factory('SocketService', require('./services/socket'))
21 | .factory('BowerService', require('./services/bower'))
22 | .factory('ProcessService', require('./services/process'))
23 | .factory('SearchService', require('./services/search'))
24 | .factory('SettingsService', require('./services/settings'))
25 | .directive('appNavigation', require('./directives/navigation'))
26 | .directive('appConsole', require('./directives/console'))
27 | .directive('appPagination', require('./directives/pagination'))
28 | .directive('appTooltip', require('./directives/tooltip'))
29 | .directive('appPopover', require('./directives/popover'))
30 | .directive('appFocus', require('./directives/focus'))
31 | .directive('appScroll', require('./directives/scroll'))
32 | .directive('appNewTab', require('./directives/new-tab'))
33 | .filter('fromNow', require('./filters/from-now'));
34 |
--------------------------------------------------------------------------------
/client/assets/scripts/controllers/pagination.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var _ = require('lodash');
4 |
5 | module.exports = [
6 | '$scope',
7 | function ($scope) {
8 |
9 | $scope._ = _;
10 |
11 | // Attributes with default value
12 | $scope.min = $scope.min || 1;
13 | $scope.offset = $scope.offset || 2;
14 |
15 | // Initialize properties
16 | $scope.init = function () {
17 | $scope.repeat = $scope.offset * 2 + 1;
18 | if ($scope.repeat > $scope.max) {
19 | $scope.repeat = $scope.max;
20 | }
21 |
22 | if ($scope.current < $scope.min + $scope.offset) {
23 | $scope.repeatStart = $scope.min;
24 | }
25 | else if ($scope.current > $scope.max - $scope.offset) {
26 | $scope.repeatStart = $scope.max - $scope.offset * 2;
27 | if ($scope.repeatStart < $scope.min) {
28 | $scope.repeatStart = $scope.min;
29 | }
30 | }
31 | else {
32 | $scope.repeatStart = $scope.current - $scope.offset;
33 | }
34 | };
35 |
36 | $scope.hasPrev = function () {
37 | return $scope.current > $scope.min;
38 | };
39 | $scope.hasNext = function () {
40 | return $scope.current < $scope.max;
41 | };
42 | $scope.hasStart = function () {
43 | return $scope.repeatStart > $scope.min;
44 | };
45 | $scope.hasEnd = function () {
46 | return $scope.repeatStart + $scope.repeat - 1 < $scope.max;
47 | };
48 | $scope.hasStartPadding = function () {
49 | return $scope.repeatStart > $scope.min + 1;
50 | };
51 | $scope.hasEndPadding = function () {
52 | return $scope.repeatStart + $scope.repeat - 1 < $scope.max - 1;
53 | };
54 |
55 | $scope.$watchGroup(['min', 'max', 'offset'], function () {
56 | $scope.init();
57 | });
58 |
59 | $scope.init();
60 |
61 | }
62 | ];
63 |
--------------------------------------------------------------------------------
/client/assets/scripts/controllers/search-results.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var _ = require('lodash');
4 |
5 | module.exports = [
6 | '$scope',
7 | '$state',
8 | 'BowerService',
9 | 'ProcessService',
10 | 'SearchService',
11 | 'SettingsService',
12 | function ($scope, $state, BowerService, ProcessService, SearchService, SettingsService) {
13 |
14 | // Properties
15 | $scope.service = SearchService;
16 | $scope.bower = BowerService;
17 | $scope.process = ProcessService;
18 | $scope.config = SettingsService.config;
19 | $scope.sorts = [
20 | {
21 | name: 'Most stars',
22 | params: {s: 'stars', o: 'desc', p: null}
23 | },
24 | {
25 | name: 'Fewest stars',
26 | params: {s: 'stars', o: 'asc', p: null}
27 | },
28 | {
29 | name: 'Package name',
30 | params: {s: 'name', o: 'asc', p: null}
31 | },
32 | {
33 | name: 'Package name (desc)',
34 | params: {s: 'name', o: 'desc', p: null}
35 | },
36 | {
37 | name: 'Owner name',
38 | params: {s: 'owner', o: 'asc', p: null}
39 | },
40 | {
41 | name: 'Owner name (desc)',
42 | params: {s: 'owner', o: 'desc', p: null}
43 | },
44 | {
45 | name: 'Recently updated',
46 | params: {s: 'updated', o: 'desc', p: null}
47 | },
48 | {
49 | name: 'Least recently updated',
50 | params: {s: 'updated', o: 'asc', p: null}
51 | }
52 | ];
53 |
54 | // Get current sort name
55 | $scope.getSortName = function () {
56 | var sort = _.find($scope.sorts, function (sort) {
57 | return sort.params.s === $scope.service.sorting && sort.params.o === $scope.service.order;
58 | });
59 | return sort.name;
60 | };
61 |
62 | // Initialize
63 | $scope.service.setParams($state.params);
64 |
65 | }
66 | ];
67 |
--------------------------------------------------------------------------------
/client/assets/scripts/services/process.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var _ = require('lodash');
4 |
5 | module.exports = [
6 | 'SocketService',
7 | function (SocketService) {
8 |
9 | var service = {
10 |
11 | // Log message
12 | log: 'Welcome to bower-browser!\n',
13 |
14 | // Process running or not
15 | running: false,
16 |
17 | // IDs of running or waiting commands
18 | queue: [],
19 |
20 | // Check ID(s) in command queue
21 | isInQueue: function (id) {
22 | var self = this;
23 | if (typeof id === 'string') {
24 | return this.queue.indexOf(id) !== -1;
25 | }
26 | if (_.isArray(id)) {
27 | return _.some(id, function (val) {
28 | return self.queue.indexOf(val) !== -1;
29 | });
30 | }
31 | return false;
32 | },
33 |
34 | // WebSocket to execute command
35 | execute: function (command, id) {
36 | id = id || '';
37 | if (id) {
38 | this.queue.push(id);
39 | }
40 | SocketService.emit('execute', {
41 | command: command,
42 | id: id
43 | });
44 | },
45 |
46 | // Push log
47 | pushLog: function (string) {
48 | this.log += string;
49 | }
50 |
51 | };
52 |
53 | // Receive WebSocket
54 | SocketService.on('log', function (message) {
55 | service.pushLog(message);
56 | });
57 | SocketService.on('added', function (id) {
58 | if (id && !service.isInQueue(id)) {
59 | service.queue.push(id);
60 | }
61 | });
62 | SocketService.on('start', function () {
63 | service.running = true;
64 | });
65 | SocketService.on('end', function (id) {
66 | if (id) {
67 | service.queue = _.without(service.queue, id);
68 | }
69 | });
70 | SocketService.on('done', function () {
71 | service.running = false;
72 | });
73 |
74 | return service;
75 |
76 | }
77 | ];
78 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 0.6.2 (2015-04-03)
4 | - [fix] Prevent serving deficient API data. ([#11](https://github.com/rakuten-frontend/bower-browser/issues/11))
5 |
6 | ## 0.6.1 (2015-04-02)
7 | - [fix] Fix scrollbar issue on IE.
8 |
9 | ## 0.6.0 (2015-03-23)
10 | - Renew the design. ([#8](https://github.com/rakuten-frontend/bower-browser/issues/8))
11 | - Improve performance of search page.
12 | - [fix] Open project links in new tab if configured.
13 | - [fix] Prevent 404 error of settings.json.
14 |
15 | ## 0.5.1 (2015-03-15)
16 | - Support Node.js 0.12 and io.js.
17 | - Show correct packages by improving dedupe function. (see: [bower/search #64](https://github.com/bower/search/pull/64))
18 |
19 | ## 0.5.0 (2015-03-12)
20 | - Show keywords in search results. ([#6](https://github.com/rakuten-frontend/bower-browser/issues/6))
21 | - Support `:` notation for search, e.g. `owner:twbs`, `keyword:responsive`.
22 | - Notify update if available.
23 | - [fix] Correct number of results per page.
24 | - [fix] Prevent resetting page number when reloading.
25 |
26 | ## 0.4.2 (2015-03-09)
27 | - Fix pagination bug.
28 |
29 | ## 0.4.1 (2015-03-06)
30 | - Tweak search for compatibility with Bower official search.
31 |
32 | ## 0.4.0 (2015-03-05)
33 | - Add "Settings" feature. ([#4](https://github.com/rakuten-frontend/bower-browser/issues/4))
34 | - Add "Uninstall without save" to the package menu.
35 | - Dedupe search results.
36 | - Update dependencies.
37 |
38 | ## 0.3.1 (2015-02-24)
39 | - Warn and exit when `bower` is not found.
40 | - Warn and exit when running with root privileges.
41 |
42 | ## 0.3.0 (2015-02-23)
43 | - Move cache file to OS's temp directory.
44 | - Support `npm install` with `sudo` by removing `postinstall` script. ([#1](https://github.com/rakuten-frontend/bower-browser/issues/1))
45 | - Optimize client source code.
46 | - Fix stdout color.
47 | - Fix icon animation on Firefox.
48 | - Refactor client scripts using Browserify.
49 | - Update dependencies.
50 |
51 | ## 0.2.0 (2015-01-27)
52 | - Provide methods and events in API.
53 | - Add `--silent` option.
54 | - Fix bugs regarding file watcher.
55 | - Test with Mocha.
56 | - Update document.
57 |
58 | ## 0.1.0 (2015-01-15)
59 | - First official release.
60 |
--------------------------------------------------------------------------------
/test/server-test.js:
--------------------------------------------------------------------------------
1 | /* jshint mocha: true */
2 | 'use strict';
3 |
4 | var assert = require('assert');
5 | var request = require('request');
6 | var _ = require('lodash');
7 |
8 | var bowerBrowser = require('../lib/');
9 |
10 | var baseOptions = {
11 | path: 'test/fixtures',
12 | open: false,
13 | silent: true
14 | };
15 |
16 | describe('Server', function () {
17 |
18 | this.timeout(10000);
19 |
20 | it('returns HTTP response', function (done) {
21 | var app = bowerBrowser(baseOptions);
22 | app.on('start', function () {
23 | request('http://localhost:3010/', function (error, res) {
24 | assert.equal(res.statusCode, 200);
25 | app.close();
26 | done();
27 | });
28 | });
29 | });
30 |
31 | it('isn\'t accessible after `close` event', function (done) {
32 | var app = bowerBrowser(baseOptions);
33 | app.on('start', function () {
34 | request('http://localhost:3010/', function (error, res) {
35 | assert.equal(res.statusCode, 200);
36 | app.close();
37 | });
38 | });
39 | app.on('close', function () {
40 | request('http://localhost:3010/', function (error) {
41 | assert(error);
42 | done();
43 | });
44 | });
45 | });
46 |
47 | it('listens specified port', function (done) {
48 | var app = bowerBrowser(_.merge({}, baseOptions, {
49 | port: 3011
50 | }));
51 | app.on('start', function () {
52 | request('http://localhost:3011/', function (error, res) {
53 | assert.equal(res.statusCode, 200);
54 | app.close();
55 | done();
56 | });
57 | });
58 | });
59 |
60 | it('fetches API and returns json', function (done) {
61 | this.timeout(20000);
62 | var app = bowerBrowser(_.merge({}, baseOptions, {
63 | cache: 0
64 | }));
65 | app.on('start', function () {
66 | request('http://localhost:3010/api/bower-component-list.json', function (error, res, body) {
67 | var data;
68 | var isJson;
69 | assert.equal(res.statusCode, 200);
70 | try {
71 | data = JSON.parse(body);
72 | isJson = typeof data === 'object' && data !== null;
73 | }
74 | catch (e) {
75 | isJson = false;
76 | }
77 | assert(isJson);
78 | app.close();
79 | done();
80 | });
81 | });
82 | });
83 |
84 | });
85 |
--------------------------------------------------------------------------------
/client/assets/scripts/services/settings.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var _ = require('lodash');
4 | _.mixin(require('lodash-deep'));
5 |
6 | var settingsApi = '/api/settings.json';
7 |
8 | module.exports = [
9 | '$http',
10 | 'SocketService',
11 | '$timeout',
12 | function ($http, SocketService, $timeout) {
13 |
14 | var defaults = {
15 | searchField: {
16 | name: true,
17 | owner: true,
18 | description: true,
19 | keyword: true
20 | },
21 | exactMatch: true,
22 | ignoreDeprecatedPackages: true,
23 | newTab: false,
24 | defaultInstallOptions: '--save',
25 | defaultInstallVersion: ''
26 | };
27 |
28 | var service = {
29 |
30 | // Active settings
31 | config: _.cloneDeep(defaults),
32 |
33 | // State for settings
34 | loaded: false,
35 |
36 | // Set settings
37 | // Invalid properties are ignored
38 | set: function (data) {
39 | var validData = _.deepMapValues(this.config, function (value, propertyPath) {
40 | return _.deepGet(data, propertyPath.join('.'));
41 | });
42 | _.merge(this.config, validData);
43 | },
44 |
45 | // Load settings from server
46 | load: function () {
47 | var self = this;
48 | $http.get(settingsApi)
49 | .success(function (data) {
50 | self.set(data);
51 | $timeout(function () {
52 | self.loaded = true;
53 | });
54 | })
55 | .error(function () {
56 | self.reset();
57 | $timeout(function () {
58 | self.loaded = true;
59 | });
60 | });
61 | },
62 |
63 | // Send settings to server
64 | save: function () {
65 | SocketService.emit('settings', this.config);
66 | },
67 |
68 | // Reset all settings to defaults
69 | reset: function () {
70 | this.set(defaults);
71 | },
72 |
73 | // Warn when no search field is selected
74 | hasSearchFieldWarning: function () {
75 | var searchField = this.config.searchField;
76 | return searchField &&
77 | Object.keys(searchField).every(function (key) {
78 | return searchField[key] === false;
79 | });
80 | }
81 |
82 | };
83 |
84 | // Initialize settings
85 | service.load();
86 |
87 | return service;
88 |
89 | }
90 | ];
91 |
--------------------------------------------------------------------------------
/client/assets/scripts/services/bower.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var _ = require('lodash');
4 |
5 | module.exports = [
6 | 'SocketService',
7 | 'ProcessService',
8 | function (SocketService, ProcessService) {
9 |
10 | var service = {
11 |
12 | // Target project
13 | name: '',
14 | path: '',
15 |
16 | // bower.json data
17 | json: {},
18 |
19 | // State and error message
20 | loaded: false,
21 | error: false,
22 | message: '',
23 |
24 | // Load Bower data
25 | load: function () {
26 | SocketService.emit('load');
27 | },
28 |
29 | // Install package
30 | install: function (pkg, options) {
31 | var name;
32 | var opts;
33 | if (typeof pkg === 'object') {
34 | opts = pkg;
35 | }
36 | else {
37 | name = pkg;
38 | opts = options || {};
39 | }
40 | var endpoint = name && opts.version ? name + '#' + opts.version : name;
41 | if (endpoint) {
42 | ProcessService.execute(['bower install', endpoint, opts.options].join(' '), 'install-' + name);
43 | }
44 | else {
45 | ProcessService.execute(['bower install', opts.options].join(' '), 'install');
46 | }
47 | },
48 |
49 | // Uninstall package
50 | uninstall: function (name, options) {
51 | var opts = options || {};
52 | ProcessService.execute(['bower uninstall', name, opts.options].join(' '), 'uninstall-' + name);
53 | },
54 |
55 | // Update package
56 | update: function () {
57 | ProcessService.execute('bower update', 'update');
58 | },
59 |
60 | // Check installation
61 | isInstalled: function (name) {
62 | if (_.has(this.json.dependencies, name)) {
63 | return 'dependencies';
64 | }
65 | if (_.has(this.json.devDependencies, name)) {
66 | return 'devDependencies';
67 | }
68 | return false;
69 | },
70 |
71 | // Get installed version
72 | getVersion: function (name, field) {
73 | field = field || 'dependencies';
74 | return this.json[field][name];
75 | }
76 |
77 | };
78 |
79 | // Receive WebSocket
80 | SocketService.on('bower', function (data) {
81 | service.loaded = true;
82 | service.name = data.name;
83 | service.path = data.path;
84 | service.json = data.json || {};
85 | service.message = data.message || '';
86 | service.error = !!data.message;
87 | });
88 |
89 | if (!service.loaded) {
90 | service.load();
91 | }
92 |
93 | return service;
94 |
95 | }
96 | ];
97 |
--------------------------------------------------------------------------------
/client/assets/templates/home.html:
--------------------------------------------------------------------------------
1 |
2 |
{{bower.json.name || bower.name}} {{bower.json.version}}
3 |
4 |
5 |
6 |
7 |
dependencies
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | | Name |
17 | Version |
18 | |
19 |
20 |
21 |
22 |
23 | | {{name}} |
24 | {{version}} |
25 | |
26 |
27 |
28 |
29 |
30 |
31 |
devDependencies
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | | Name |
41 | Version |
42 | |
43 |
44 |
45 |
46 |
47 | | {{name}} |
48 | {{version}} |
49 | |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
Failed to load bower.json
{{bower.message}}
61 |
Reload
62 |
63 |
64 |
--------------------------------------------------------------------------------
/client/assets/templates/settings.html:
--------------------------------------------------------------------------------
1 | Settings
2 |
39 |
40 |
Bower
41 |
47 |
53 |
54 |
55 |
Reset settings
56 |
62 |
63 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "bower-browser",
3 | "version": "0.6.2",
4 | "description": "GUI Bower manager runs on web browser",
5 | "author": {
6 | "name": "Rakuten, Inc."
7 | },
8 | "license": "MIT",
9 | "repository": {
10 | "type": "git",
11 | "url": "https://github.com/rakuten-frontend/bower-browser.git"
12 | },
13 | "homepage": "https://github.com/rakuten-frontend/bower-browser",
14 | "bugs": {
15 | "url": "https://github.com/rakuten-frontend/bower-browser/issues"
16 | },
17 | "keywords": [
18 | "bower",
19 | "gui",
20 | "server",
21 | "app"
22 | ],
23 | "engines": {
24 | "node": ">=0.10.0"
25 | },
26 | "scripts": {
27 | "test": "gulp test",
28 | "prepublish": "gulp"
29 | },
30 | "main": "lib",
31 | "bin": {
32 | "bower-browser": "bin/bower-browser"
33 | },
34 | "files": [
35 | "bin",
36 | "lib"
37 | ],
38 | "dependencies": {
39 | "async": "^0.9.0",
40 | "chalk": "^1.0.0",
41 | "commander": "^2.5.0",
42 | "connect": "^3.3.3",
43 | "gaze": "^0.5.1",
44 | "is-root": "^1.0.0",
45 | "lodash": "^3.0.0",
46 | "mkdirp": "^0.5.0",
47 | "opn": "^1.0.0",
48 | "request": "^2.51.0",
49 | "serve-static": "^1.8.0",
50 | "socket.io": "^1.2.1",
51 | "update-notifier": "^0.3.1",
52 | "which": "^1.0.8",
53 | "win-spawn": "^2.0.0"
54 | },
55 | "devDependencies": {
56 | "angular": "~1.3.13",
57 | "angular-scroll": "^0.6.4",
58 | "angular-ui-router": "^0.2.13",
59 | "bootstrap-sass": "~3.3.3",
60 | "brfs": "^1.3.0",
61 | "browserify": "^9.0.3",
62 | "browserify-shim": "^3.8.2",
63 | "del": "^1.1.1",
64 | "gulp": "^3.8.10",
65 | "gulp-autoprefixer": "^2.0.0",
66 | "gulp-if": "^1.2.5",
67 | "gulp-jscs": "^1.3.1",
68 | "gulp-jshint": "^1.9.0",
69 | "gulp-livereload": "^3.7.0",
70 | "gulp-load-plugins": "^0.10.0",
71 | "gulp-minify-css": "^1.0.0",
72 | "gulp-mocha": "^2.0.0",
73 | "gulp-nodemon": "^2.0.2",
74 | "gulp-ruby-sass": "^1.0.0-alpha",
75 | "gulp-sourcemaps": "^1.2.8",
76 | "gulp-uglify": "^1.1.0",
77 | "gulp-useref": "^1.1.1",
78 | "gulp-util": "^3.0.3",
79 | "jquery": "~2.1.3",
80 | "jshint-stylish": "^1.0.0",
81 | "lodash-deep": "^1.5.3",
82 | "minimist": "^1.1.0",
83 | "moment": "^2.9.0",
84 | "multipipe": "^0.1.2",
85 | "run-sequence": "^1.0.2",
86 | "socket.io-client": "^1.3.4",
87 | "vinyl-buffer": "^1.0.0",
88 | "vinyl-source-stream": "^1.0.0",
89 | "watchify": "^3.2.0"
90 | },
91 | "browserify": {
92 | "transform": [
93 | "browserify-shim"
94 | ]
95 | },
96 | "browser": {
97 | "angular": "./node_modules/angular/angular.js",
98 | "angular-scroll": "./node_modules/angular-scroll/angular-scroll.js"
99 | },
100 | "browserify-shim": {
101 | "angular": {
102 | "exports": "angular",
103 | "depends": [
104 | "jquery:jQuery"
105 | ]
106 | },
107 | "angular-scroll": {
108 | "exports": "angular.module('duScroll').name",
109 | "depends": [
110 | "angular"
111 | ]
112 | },
113 | "bootstrap-sass": {
114 | "depends": [
115 | "jquery:jQuery"
116 | ]
117 | }
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/client/assets/scripts/values/ignore.js:
--------------------------------------------------------------------------------
1 | /**
2 | * List of ignoring packages
3 | * (c) The Bower team
4 | * https://github.com/bower/search/blob/gh-pages/js/config/ignore.js
5 | */
6 |
7 | 'use strict';
8 |
9 | // Ignore these package names
10 | module.exports = [
11 | 'Chart.js',
12 | 'coffeescript',
13 | 'linux',
14 | 'express',
15 | 'phantom',
16 | 'homebrew',
17 | 'jade',
18 | 'jasmine',
19 | 'angulerjs',
20 | 'web-starter-kit',
21 | 'EaselJS',
22 | 'EpicEditor',
23 | 'FitVids',
24 | 'Flat-UI',
25 | 'FlowTypeJS',
26 | 'Flowtype.js',
27 | 'Font-Awesome',
28 | 'Ladda',
29 | 'Metro-UI-CSS',
30 | 'PhysicsJS',
31 | 'ResponsiveSlides',
32 | 'Semantic-UI',
33 | 'SlidesJs',
34 | 'Snap.svg',
35 | 'Swipe',
36 | 'URIjs',
37 | 'abcdef1234567890',
38 | 'angular-1.1.6',
39 | 'angular-boostrap-ui',
40 | 'backbone.marionette',
41 | 'blueimp-file-upload',
42 | 'boostrap-sass',
43 | 'bootstrap-3-datepicker',
44 | 'bootstrap-ui',
45 | 'bxslider-4',
46 | 'flowtype',
47 | 'fullPage.js',
48 | 'grunt',
49 | 'gulp',
50 | 'handlebars-wycats',
51 | 'handson-table',
52 | 'history',
53 | 'inuitcss',
54 | 'jQuery',
55 | 'jknob',
56 | 'jquery-fittext.js',
57 | 'jquery-flot',
58 | 'jquery.cookie',
59 | 'jquery_validation',
60 | 'jsPlumb',
61 | 'ladda-boostrap-hakimel',
62 | 'letteringjs',
63 | 'momentjs',
64 | 'nnnick-chartjs',
65 | 'normalize-css',
66 | 'pixi',
67 | 'request',
68 | 'respondJS',
69 | 'respondJs',
70 | 'responsiveSlides.js',
71 | 'scrollReveal.js',
72 | 'scrollr',
73 | 'semantic',
74 | 'sir-trevor',
75 | 'soundmanager2',
76 | 'spinjs',
77 | 'store.js',
78 | 'tinyicon',
79 | 'turnjs',
80 | 'twbs-bootstrap-sass',
81 | 'twitter',
82 | 'ui-bootstrap',
83 | 'ui.bootstrap',
84 | 'yui',
85 | 'grunt',
86 | 'grunt-blanket-mocha',
87 | 'grunt-contrib-jshint',
88 | 'grunt-contrib-watch',
89 | 'grunt-contrib-uglify',
90 | 'grunt-contrib-concat',
91 | 'grunt-contrib-jasmine',
92 | 'grunt-contrib-less',
93 | 'grunt-cli',
94 | 'grunt-karma',
95 | 'grunt-contrib-compass',
96 | 'grunt-mocha',
97 | 'grunt-contrib-sass',
98 | 'grunt-contrib-clean',
99 | 'grunt-shell',
100 | 'grunt-contrib-copy',
101 | 'grunt-contrib-cssmin',
102 | 'grunt-contrib-connect',
103 | 'grunt-contrib-coffee',
104 | 'grunt-contrib-imagemin',
105 | 'grunt-autoprefixer',
106 | 'grunt-contrib-qunit',
107 | 'grunt-contrib-nodeunit',
108 | 'time-grunt',
109 | 'grunt-usemin',
110 | 'grunt-junit',
111 | 'grunt-contrib-requirejs',
112 | 'grunt-modernizr',
113 | 'grunt-contrib-htmlmin',
114 | 'grunt-concurrent',
115 | 'grunt-contrib-compress',
116 | 'grunt-requirejs',
117 | 'grunt-contrib-jade',
118 | 'grunt-newer',
119 | 'grunt-processhtml',
120 | 'grunt-template',
121 | 'Grunt-Workflow',
122 | 'grunt-svgmin',
123 | 'grunt-livescript',
124 | 'grunt-hack',
125 | 'grunt-dustjs-linkedin',
126 | 'MonkeytestJS',
127 | 'grunt-workflow',
128 | 'grunt-contrib-livereload',
129 | 'grunt-git-clean',
130 | 'grunt-init-assemble-helper',
131 | 'another-grunt-test',
132 | 'grunt-html-prettyprinter',
133 | 'grunt-rev',
134 | 'grunt-wildamd',
135 | 'grunt-if-missing',
136 | 'anila-grunt-test',
137 | 'grunt-contrib-stylus',
138 | 'grunt-contrib-handlebars',
139 | 'polybrick',
140 | 'grunt-contrib-csslint',
141 | 'grunt-contrib-symlink',
142 | 'grunt-init-jquery',
143 | 'sass-grunt-livereload',
144 | 'curist/grunt-bower',
145 | 'cosium-grunt-connect-proxy',
146 | 'grunt-contrib-jst',
147 | 'grunt-contrib-yuidoc',
148 | 'grunt-cdn',
149 | 'grunt-contrib-bump',
150 | 'grunt-modular-project-tasks',
151 | 'grunt-contrib-mincss',
152 | 'libsass-grunt-livereload',
153 | 'grunt-contrib-rquirejs',
154 | 'mockserver-grunt',
155 | 'Grunted-Front',
156 | 'grunt-contrib-internal',
157 | 'releaseable-test',
158 | 'grunt-scp',
159 | 'grunt-matsuo-utils',
160 | 'grunt-contrib-bobtail',
161 | 'sms-grunt-boilerplate',
162 | 'spa-bootstrap',
163 | 'grunt-videojs-languages'
164 | ];
165 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # bower-browser
2 |
3 | > GUI Bower manager runs on web browser
4 |
5 | [![NPM Version][npm-image]][npm-url]
6 | [![Build Status][travis-image]][travis-url]
7 | [![Dependency Status][deps-image]][deps-url]
8 |
9 | 
10 |
11 | ## Features
12 | * Search from the Bower registry
13 | * Install packages with various options
14 | * Monitor log in realtime
15 | * Manage local Bower components
16 |
17 | 
18 |
19 | ## Installation
20 | Install via npm.
21 |
22 | ```shell
23 | $ npm install -g bower-browser
24 | ```
25 |
26 | Install with `-g` option for command line interface, `--save` or `--save-dev` for using [module API](#api).
27 | [Grunt plugin](https://github.com/rakuten-frontend/grunt-bower-browser) is also available.
28 |
29 | ### Requirements
30 | * [Node.js](https://nodejs.org/) or [io.js](https://iojs.org/)
31 | * [Bower](http://bower.io/) and [Git](http://git-scm.com/)
32 | * Modern web browser (IE10+ supported)
33 |
34 | bower-browser executes `bower` in background.
35 | Make sure to install Bower if you haven't: `$ npm install -g bower`
36 |
37 | ## Usage
38 | ```shell
39 | $ cd path/to/your-project
40 | $ bower-browser
41 | ```
42 |
43 | Then, web browser will open `http://localhost:3010` automatically.
44 | Manage your Bower components in the web GUI! :-)
45 |
46 | ### CLI Options
47 | * `--path `
48 | Location of bower.json. (default: use `process.cwd()`)
49 |
50 | * `--port `
51 | Port number of bower-browser server. (default: `3010`)
52 |
53 | * `--cache `
54 | Cache TTL for package list API. Set `0` to force to fetch API. (default: `86400` = 24hours)
55 |
56 | * `--skip-open`
57 | Prevent opening web browser at the start.
58 |
59 | * `--silent`
60 | Print nothing to stdout.
61 |
62 | * `-h`, `--help`
63 | Output usage information.
64 |
65 | * `-V`, `--version`
66 | Output the version number.
67 |
68 | ## Integration with Build Systems
69 |
70 | ### Gulp
71 | Use `bower-browser` module directly.
72 |
73 | ```javascript
74 | var bowerBrowser = require('bower-browser');
75 |
76 | gulp.task('bower-browser', function () {
77 | bowerBrowser({
78 | // Options here.
79 | });
80 | });
81 |
82 | // Alias for running preview server and bower-browser at the same time.
83 | gulp.task('serve', ['connect', 'bower-browser', 'watch'], function () {
84 | // ...
85 | });
86 | ```
87 |
88 | ### Grunt
89 | Use [grunt-bower-browser](https://github.com/rakuten-frontend/grunt-bower-browser) plugin.
90 |
91 | ## API
92 |
93 | ### Quick Start
94 | ```javascript
95 | // Run bower-browser using default config.
96 | require('bower-browser')();
97 | ```
98 |
99 | ### Advanced
100 | ```javascript
101 | var bowerBrowser = require('bower-browser');
102 |
103 | // Start app with options you like.
104 | var app = bowerBrowser({
105 | path: 'path/to/project', // Location of bower.json. default: null (use process.cwd())
106 | port: 8080, // Port number. default: 3010
107 | cache: 0, // Cache TTL. Set 0 to force to fetch API. default: 86400 (24hrs)
108 | open: false, // Prevent opening browser. default: true (open automatically)
109 | silent: true // Print nothing to stdout. default: false
110 | });
111 |
112 | // Events
113 | app.on('start', function () {
114 | console.log('Started bower-browser!');
115 | });
116 |
117 | // Methods
118 | app.close();
119 | ```
120 |
121 | **NOTE: Events and methods are experimental for now. They might be updated.**
122 |
123 | #### Events
124 | * `on('start', callback)`
125 | When the web server is started.
126 |
127 | * `on('close', callback)`
128 | When the web server and all wathers are closed.
129 |
130 | * `on('log', callback(message))`
131 | When log message is received from bower execution.
132 |
133 | #### Methods
134 | * `close()`
135 | Close web server and all watchers.
136 |
137 | ## License
138 | Copyright (c) 2014-2015 Rakuten, Inc. Licensed under the [MIT License](LICENSE).
139 |
140 | [npm-image]: https://img.shields.io/npm/v/bower-browser.svg?style=flat
141 | [npm-url]: https://www.npmjs.com/package/bower-browser
142 | [travis-image]: https://img.shields.io/travis/rakuten-frontend/bower-browser/master.svg?style=flat
143 | [travis-url]: https://travis-ci.org/rakuten-frontend/bower-browser
144 | [deps-image]: http://img.shields.io/david/rakuten-frontend/bower-browser.svg?style=flat
145 | [deps-url]: https://david-dm.org/rakuten-frontend/bower-browser
146 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var gulp = require('gulp');
4 | var $ = require('gulp-load-plugins')();
5 | var source = require('vinyl-source-stream');
6 | var buffer = require('vinyl-buffer');
7 | var pipe = require('multipipe');
8 | var runSequence = require('run-sequence');
9 | var browserify = require('browserify');
10 | var brfs = require('brfs');
11 | var watchify = require('watchify');
12 | var del = require('del');
13 | var minimist = require('minimist');
14 |
15 | var knownOptions = {
16 | string: 'env',
17 | default: {
18 | env: process.env.NODE_ENV || 'production'
19 | }
20 | };
21 | var options = minimist(process.argv.slice(2), knownOptions);
22 |
23 | var paths = {
24 | src: './client',
25 | dest: './lib/public',
26 | test: './test',
27 | scripts: [
28 | './lib/*.js',
29 | './test/*.js',
30 | './client/assets/scripts/**/*.js'
31 | ],
32 | styles: [
33 | './client/assets/styles/*.scss'
34 | ],
35 | html: [
36 | './client/*.html'
37 | ]
38 | };
39 |
40 | gulp.task('clean', function (callback) {
41 | del(paths.dest, callback);
42 | });
43 |
44 | gulp.task('lint', function () {
45 | return pipe(
46 | gulp.src(paths.scripts),
47 | $.jscs(),
48 | $.jshint(),
49 | $.jshint.reporter('jshint-stylish'),
50 | $.jshint.reporter('fail')
51 | );
52 | });
53 |
54 | gulp.task('mocha', function () {
55 | return gulp.src(paths.test + '/*-test.js', {read: false})
56 | .pipe($.mocha({reporter: 'spec'}))
57 | .once('error', function () {
58 | process.exit(1);
59 | })
60 | .once('end', function () {
61 | process.exit();
62 | });
63 | });
64 |
65 | function buildScripts(watch) {
66 | var baseArgs = {
67 | entries: [paths.src + '/assets/scripts/app.js'],
68 | debug: true
69 | };
70 | var bundler = watch ? watchify(browserify(baseArgs, watchify.args)) : browserify(baseArgs);
71 | var bundle = function () {
72 | return bundler.bundle()
73 | .on('error', function (error) {
74 | $.util.log($.util.colors.red('Browserify error:') + '\n' + error.message);
75 | })
76 | .pipe(source('app.js'))
77 | .pipe(buffer())
78 | .pipe($.sourcemaps.init({loadMaps: true}))
79 | .pipe($.if(options.env === 'production', $.uglify()))
80 | .pipe($.sourcemaps.write('./'))
81 | .pipe(gulp.dest(paths.dest + '/assets/scripts'));
82 | };
83 | bundler.transform(brfs);
84 | if (watch) {
85 | bundler
86 | .on('update', bundle)
87 | .on('log', function (message) {
88 | $.util.log('Browserify log:\n' + message);
89 | });
90 | }
91 | return bundle();
92 | }
93 |
94 | gulp.task('scripts', function () {
95 | return buildScripts();
96 | });
97 |
98 | gulp.task('scripts:watch', function () {
99 | return buildScripts(true);
100 | });
101 |
102 | gulp.task('styles', function () {
103 | return $.rubySass(paths.src + '/assets/styles/', {
104 | loadPath: './node_modules',
105 | style: 'expanded',
106 | sourcemap: true
107 | })
108 | .on('error', function (error) {
109 | $.util.log($.util.colors.red('Sass error:') + '\n' + error.message);
110 | })
111 | .pipe($.autoprefixer())
112 | .pipe($.if(options.env === 'production', $.minifyCss({
113 | keepSpecialComments: 0,
114 | advanced: false
115 | })))
116 | .pipe($.sourcemaps.write('./'))
117 | .pipe(gulp.dest(paths.dest + '/assets/styles'));
118 | });
119 |
120 | gulp.task('fonts', function () {
121 | return gulp.src([
122 | './node_modules/bootstrap-sass/assets/fonts/bootstrap/*'
123 | ])
124 | .pipe(gulp.dest(paths.dest + '/assets/fonts'));
125 | });
126 |
127 | gulp.task('html', function () {
128 | return gulp.src(paths.html)
129 | .pipe($.if(options.env === 'production', $.useref()))
130 | .pipe(gulp.dest(paths.dest));
131 | });
132 |
133 | gulp.task('extras', function () {
134 | return gulp.src([
135 | 'favicon.ico',
136 | 'apple-touch-icon.png'
137 | ], {
138 | cwd: paths.src
139 | })
140 | .pipe(gulp.dest(paths.dest));
141 | });
142 |
143 | gulp.task('nodemon', function () {
144 | return $.nodemon({
145 | script: paths.test + '/server.js',
146 | ignore: [
147 | paths.src,
148 | paths.dest,
149 | paths.test
150 | ]
151 | });
152 | });
153 |
154 | gulp.task('watch', function () {
155 | gulp.watch(paths.scripts, ['lint']);
156 | gulp.watch(paths.styles, ['styles']);
157 | gulp.watch(paths.html, ['html']);
158 | gulp.watch([
159 | '*.html',
160 | 'assets/scripts/**/*.js',
161 | 'assets/styles/*.css',
162 | 'assets/fonts/*'
163 | ], {
164 | cwd: paths.dest
165 | })
166 | .on('change', $.livereload.changed);
167 | });
168 |
169 | gulp.task('serve', ['clean'], function (callback) {
170 | options.env = 'development';
171 | $.livereload.listen();
172 | runSequence(['scripts:watch', 'styles', 'fonts', 'html', 'extras'], 'nodemon', 'watch', callback);
173 | });
174 |
175 | gulp.task('build', ['scripts', 'styles', 'fonts', 'html', 'extras']);
176 |
177 | gulp.task('test', ['clean'], function (callback) {
178 | runSequence('lint', 'build', 'mocha', callback);
179 | });
180 |
181 | gulp.task('default', ['test']);
182 |
--------------------------------------------------------------------------------
/client/assets/templates/search-results.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{service.from | number}}-{{service.to | number}} of {{service.count | number}} results
5 |
6 |
7 |
8 |
9 |
13 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
29 |
{{::component.description}}
30 |
31 |
36 |
37 | - {{::component.stars | number}}
38 | - Updated {{::component.updated | fromNow}}
39 |
40 |
41 |
42 |
76 |
77 |
78 |
79 | Loading...
80 |
81 |
82 | Your search "{{service.query}}" did not match any packages.
83 |
84 |
85 | Failed to load package list
86 | Please restart bower-browser with --cache 0 option.
87 |
88 |
89 |
92 |
--------------------------------------------------------------------------------
/client/assets/scripts/services/search.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var _ = require('lodash');
4 |
5 | var ignore = require('../values/ignore');
6 | var whitelist = require('../values/whitelist');
7 |
8 | var api = '/api/bower-component-list.json';
9 |
10 | module.exports = [
11 | '$http',
12 | 'SettingsService',
13 | function ($http, SettingsService) {
14 |
15 | var defaultParams = {
16 | query: '',
17 | page: 1,
18 | sorting: 'stars',
19 | order: 'desc'
20 | };
21 | var config = SettingsService.config;
22 | var packages = [];
23 |
24 | var service = {
25 |
26 | // Properties
27 | results: [],
28 | searching: false,
29 | loaded: false,
30 | loadingError: false,
31 | page: defaultParams.page,
32 | count: 0,
33 | pageCount: 1,
34 | from: 0,
35 | to: 0,
36 | limit: 20,
37 | sorting: defaultParams.sorting,
38 | order: defaultParams.order,
39 | query: defaultParams.query,
40 |
41 | // Set params and update results
42 | setParams: function (params) {
43 | var self = this;
44 | this.parseParams(params);
45 | if (!this.loaded) {
46 | this.fetchApi(api).success(function (data) {
47 | packages = data;
48 | self.loaded = true;
49 | self.search();
50 | });
51 | }
52 | else {
53 | this.search();
54 | }
55 | },
56 |
57 | // Parse params to set correct value
58 | parseParams: function (params) {
59 | this.query = params.q !== undefined ? String(params.q) : defaultParams.query;
60 | this.page = params.p !== undefined ? parseInt(params.p, 10) : defaultParams.page;
61 | switch (params.s) {
62 | case 'name':
63 | case 'owner':
64 | case 'stars':
65 | case 'updated':
66 | this.sorting = params.s;
67 | break;
68 | default:
69 | this.sorting = defaultParams.sorting;
70 | }
71 | switch (params.o) {
72 | case 'asc':
73 | case 'desc':
74 | this.order = params.o;
75 | break;
76 | default:
77 | this.order = defaultParams.order;
78 | }
79 | },
80 |
81 | // Get component list from API
82 | fetchApi: function (url) {
83 | var self = this;
84 | this.searching = true;
85 | this.loadingError = false;
86 | return $http.get(url)
87 | .success(function (res) {
88 | self.searching = false;
89 | return res.data;
90 | })
91 | .error(function () {
92 | self.searching = false;
93 | self.loadingError = true;
94 | return false;
95 | });
96 | },
97 |
98 | // Search packages
99 | search: function () {
100 | var matchedItems = packages;
101 | var parsed = this.parseQuery(this.query);
102 | var query = parsed.query;
103 | var fields = parsed.field ? [parsed.field] :
104 | _.filter(Object.keys(config.searchField), function (key) {
105 | return config.searchField[key];
106 | });
107 | var exact = !!parsed.field;
108 |
109 | matchedItems = this.filter(matchedItems);
110 | matchedItems = this.find(matchedItems, query, fields, exact);
111 | matchedItems = this.dedupe(matchedItems);
112 | matchedItems = this.sort(matchedItems, this.sorting, this.order);
113 | matchedItems = !exact ? this.prioritize(matchedItems, query) : matchedItems;
114 |
115 | this.count = matchedItems.length;
116 | this.pageCount = Math.ceil(this.count / this.limit);
117 | this.from = (this.page - 1) * this.limit + 1;
118 | this.to = this.from + this.limit - 1 > this.count ? this.count : this.from + this.limit - 1;
119 | this.results = matchedItems.slice(this.from - 1, this.to);
120 | },
121 |
122 | // Parse query string to {query,field} object
123 | parseQuery: function (query) {
124 | var fieldList = ['name', 'owner', 'description', 'keyword'];
125 | var parsedField = _.find(fieldList, function (field) {
126 | return query.indexOf(field + ':') === 0;
127 | });
128 | var parsedQuery = parsedField ? query.replace(new RegExp('^' + parsedField + ':'), '') : query;
129 | return {query: parsedQuery, field: parsedField};
130 | },
131 |
132 | // Exclude ignoring packages
133 | filter: function (items) {
134 | if (!config.ignoreDeprecatedPackages) {
135 | return items;
136 | }
137 | var list = _.filter(items, function (item) {
138 | // Ignore packages
139 | if (ignore.indexOf(item.name) !== -1) {
140 | return false;
141 | }
142 | // Limit to whitelisted packages
143 | if (whitelist[item.website] && item.name !== whitelist[item.website]) {
144 | return false;
145 | }
146 | return true;
147 | });
148 | return list;
149 | },
150 |
151 | // Dedupe packages
152 | dedupe: function (items) {
153 | if (!config.ignoreDeprecatedPackages) {
154 | return items;
155 | }
156 | var groupedResults = _.groupBy(items.reverse(), function (item) {
157 | return item.website;
158 | });
159 | var list = [];
160 | _.forEach(groupedResults, function (group) {
161 | var matchedItem;
162 | if (group.length > 1) {
163 | var repoName = group[0].website.split('/').pop();
164 | matchedItem = _.find(group, function (item) {
165 | return item.name === repoName;
166 | });
167 | }
168 | if (!matchedItem) {
169 | matchedItem = group[0];
170 | }
171 | list.push(matchedItem);
172 | });
173 | return list;
174 | },
175 |
176 | // Find items by query
177 | find: function (items, query, fields, exact) {
178 | var self = this;
179 | var isTarget = function (fieldName) {
180 | return fields.indexOf(fieldName) !== -1;
181 | };
182 | if (query === '') {
183 | return items;
184 | }
185 | fields = fields || ['name', 'owner', 'description', 'keyword'];
186 | return _.filter(items, function (item) {
187 | if ((isTarget('name') && self.matchedInString(query, item.name, exact)) ||
188 | (isTarget('owner') && self.matchedInString(query, item.owner, exact)) ||
189 | (isTarget('description') && self.matchedInString(query, item.description, exact)) ||
190 | (isTarget('keyword') && self.matchedInArray(query, item.keywords, exact))) {
191 | return true;
192 | }
193 | return false;
194 | });
195 | },
196 |
197 | // Search in string field
198 | matchedInString: function (query, string, exact) {
199 | if (typeof string !== 'string' || string === '') {
200 | return false;
201 | }
202 | if (exact) {
203 | return string.toLowerCase() === query.toLowerCase();
204 | }
205 | return string.toLowerCase().indexOf(query.toLowerCase()) !== -1;
206 | },
207 |
208 | // Search in array field
209 | matchedInArray: function (query, array, exact) {
210 | if (!_.isArray(array) || array.length === 0) {
211 | return false;
212 | }
213 | return array.some(function (string) {
214 | if (exact) {
215 | return query.toLowerCase() === string.toLowerCase();
216 | }
217 | return string.toLowerCase().indexOf(query.toLowerCase()) !== -1;
218 | });
219 | },
220 |
221 | // Sort items
222 | sort: function (items, sorting, order) {
223 | var list = _.sortBy(items, function (item) {
224 | return item[sorting];
225 | });
226 | if (order === 'desc') {
227 | list = list.reverse();
228 | }
229 | return list;
230 | },
231 |
232 | // Prioritize exact match
233 | prioritize: function (items, query) {
234 | if (!config.exactMatch || !config.searchField.name) {
235 | return items;
236 | }
237 | var list = items;
238 | var match = _.findIndex(list, function (item) {
239 | return query.toLowerCase() === item.name.toLowerCase();
240 | });
241 | if (match !== -1) {
242 | list.splice(0, 0, list.splice(match, 1)[0]);
243 | }
244 | return list;
245 | }
246 |
247 | };
248 |
249 | return service;
250 |
251 | }
252 | ];
253 |
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var util = require('util');
4 | var EventEmitter = require('events').EventEmitter;
5 | var path = require('path');
6 | var fs = require('fs');
7 | var os = require('os');
8 | var spawn = require('win-spawn');
9 | var _ = require('lodash');
10 | var async = require('async');
11 | var opn = require('opn');
12 | var Gaze = require('gaze').Gaze;
13 | var request = require('request');
14 | var mkdirp = require('mkdirp');
15 | var chalk = require('chalk');
16 |
17 | var verifyEnv = require('./utils/verify-env');
18 | var isNewFile = require('./utils/is-new-file');
19 | var isJsonFile = require('./utils/is-json-file');
20 | var pkg = require('../package.json');
21 |
22 | var cwd;
23 | var basename;
24 | var jsonPath;
25 | var server;
26 | var io;
27 | var gaze;
28 | var commandQueue;
29 |
30 | var apiUrl = 'https://bower-component-list.herokuapp.com/';
31 | var tmpDir = path.join(os.tmpdir(), pkg.name);
32 | var apiCacheFile = path.join(tmpDir, 'bower-component-list.json');
33 | var settingsFile = path.join(tmpDir, 'settings.json');
34 | var defaults = {
35 | path: null,
36 | port: 3010,
37 | cache: 86400, // 86400s = 24hours
38 | open: true,
39 | silent: false
40 | };
41 |
42 | // Constructor
43 | var BowerBrowser = function (options) {
44 | var self = this;
45 | var warnings = verifyEnv();
46 |
47 | EventEmitter.call(this);
48 |
49 | this.options = _.merge({}, defaults, options);
50 | this.running = false;
51 |
52 | if (warnings) {
53 | warnings.forEach(function (warning) {
54 | self.print(warning);
55 | });
56 | process.exit(1);
57 | }
58 |
59 | this.setup();
60 |
61 | if (this.options.cache === 0 || !this.hasApiCache()) {
62 | this.fetch(function () {
63 | self.start();
64 | });
65 | }
66 | else {
67 | // Start server after the constructor
68 | setTimeout(function () {
69 | self.start();
70 | }, 0);
71 | }
72 | };
73 |
74 | // Inherit EventEmitter
75 | util.inherits(BowerBrowser, EventEmitter);
76 |
77 | // Setup local files
78 | BowerBrowser.prototype.setup = function () {
79 | var hasSettings;
80 | mkdirp.sync(tmpDir);
81 | try {
82 | hasSettings = fs.statSync(settingsFile).isFile();
83 | }
84 | catch (e) {
85 | hasSettings = false;
86 | }
87 | if (!hasSettings) {
88 | this.saveSettings({});
89 | }
90 | };
91 |
92 | // Start server
93 | BowerBrowser.prototype.start = function () {
94 | var self = this;
95 | var app = require('connect')();
96 | var serveStatic = require('serve-static');
97 |
98 | // Target project
99 | cwd = this.options.path ? path.resolve(this.options.path) : process.cwd();
100 | basename = path.basename(cwd);
101 | jsonPath = path.join(cwd, 'bower.json');
102 |
103 | // Start HTTP server
104 | server = require('http').Server(app);
105 | io = require('socket.io')(server);
106 | server.listen(this.options.port, 'localhost');
107 | app.use(serveStatic(path.join(__dirname, 'public')));
108 | app.use('/api', serveStatic(tmpDir));
109 |
110 | // Queue for sequential command execution
111 | commandQueue = async.queue(function (data, callback) {
112 | self.execute(data.command, data.id, callback);
113 | }, 1);
114 | commandQueue.drain = function () {
115 | io.sockets.emit('done');
116 | };
117 |
118 | // Handle WebSocket
119 | io.on('connection', function (socket) {
120 | self.sendBowerData(socket);
121 | socket.on('execute', function (data) {
122 | self.register(data.command, data.id);
123 | });
124 | socket.on('load', function () {
125 | self.sendBowerData(socket);
126 | });
127 | socket.on('settings', function (data) {
128 | self.saveSettings(data);
129 | });
130 | });
131 |
132 | this.running = true;
133 | this.print('Started bower-browser on ' + chalk.magenta('http://localhost:' + this.options.port) + '\n');
134 | this.emit('start');
135 | if (this.options.open) {
136 | this.open();
137 | }
138 | };
139 |
140 | // Fetch package list from the Bower registry
141 | BowerBrowser.prototype.fetch = function (callback) {
142 | var self = this;
143 | var i = 0;
144 | var processMessage = 'Fetching package list';
145 | var timer;
146 |
147 | if (!this.options.silent) {
148 | timer = setInterval(function () {
149 | process.stdout.clearLine();
150 | process.stdout.cursorTo(0);
151 | i = (i + 1) % 4;
152 | var dots = new Array(i + 1).join('.');
153 | process.stdout.write(processMessage + dots);
154 | }, 500);
155 | }
156 |
157 | request({
158 | url: apiUrl,
159 | gzip: true
160 | }, function (error) {
161 | if (!self.options.silent) {
162 | clearInterval(timer);
163 | process.stdout.clearLine();
164 | process.stdout.cursorTo(0);
165 | process.stdout.write(processMessage + '... ');
166 | }
167 | if (error) {
168 | self.print(chalk.red('Error!') + '\nCouldn\'t fetch package list from the Bower registry.\nPlease try again later.\n');
169 | return;
170 | }
171 | self.print(chalk.green('Complete!') + '\n');
172 | if (typeof callback === 'function') {
173 | callback();
174 | }
175 | })
176 | .pipe(fs.createWriteStream(apiCacheFile));
177 | };
178 |
179 | // API cache is effective or not
180 | BowerBrowser.prototype.hasApiCache = function () {
181 | if (!isNewFile(apiCacheFile, this.options.cache)) {
182 | return false;
183 | }
184 | if (!isJsonFile(apiCacheFile)) {
185 | return false;
186 | }
187 | return true;
188 | };
189 |
190 | // Open application in browser
191 | BowerBrowser.prototype.open = function () {
192 | opn('http://localhost:' + this.options.port);
193 | };
194 |
195 | // Send bower data to client(s)
196 | BowerBrowser.prototype.sendBowerData = function (socket) {
197 | this.readBowerJson(function (error, json) {
198 | var sender = socket || io.sockets;
199 | var data = {
200 | name: basename,
201 | path: cwd
202 | };
203 | if (error) {
204 | data.message = error.toString();
205 | }
206 | else {
207 | data.json = json;
208 | }
209 | sender.emit('bower', data);
210 | });
211 | };
212 |
213 | // Read and watch bower.json
214 | BowerBrowser.prototype.readBowerJson = function (callback) {
215 | var self = this;
216 | var json = null;
217 | var error = null;
218 | try {
219 | var buffer = fs.readFileSync(jsonPath);
220 | if (!gaze) {
221 | gaze = new Gaze('bower.json', {cwd: cwd});
222 | gaze.on('all', function () {
223 | self.sendBowerData();
224 | });
225 | }
226 | json = JSON.parse(buffer);
227 | }
228 | catch (e) {
229 | error = e;
230 | }
231 | if (typeof callback === 'function') {
232 | callback(error, json);
233 | }
234 | };
235 |
236 | // Register command to queue
237 | BowerBrowser.prototype.register = function (command, id) {
238 | id = id || '';
239 | commandQueue.push({
240 | command: command,
241 | id: id
242 | });
243 | io.sockets.emit('added', id);
244 | };
245 |
246 | // Execute command
247 | BowerBrowser.prototype.execute = function (input, id, callback) {
248 | var self = this;
249 | var inputArray = input.trim().split(/\s+/);
250 | var command = inputArray.shift();
251 | var args = inputArray;
252 | var options = {
253 | cwd: cwd,
254 | env: process.env
255 | };
256 | var child = spawn(command, args, options);
257 |
258 | io.sockets.emit('start', id);
259 | this.log('\n> ' + input + '\n');
260 |
261 | child.stdout.on('data', function (data) {
262 | self.log(data.toString());
263 | });
264 | child.stderr.on('data', function (data) {
265 | self.log(data.toString());
266 | });
267 | child.on('exit', function () {
268 | io.sockets.emit('end', id);
269 | if (callback) {
270 | callback();
271 | }
272 | });
273 | };
274 |
275 | // Save client settings
276 | BowerBrowser.prototype.saveSettings = function (data) {
277 | try {
278 | fs.writeFileSync(settingsFile, JSON.stringify(data, null, 2));
279 | }
280 | catch (e) {
281 | this.print(chalk.red('Failed to save settings!') + '\n' + e.message + '\n');
282 | }
283 | };
284 |
285 | // Log to stdout and socket
286 | BowerBrowser.prototype.log = function (message) {
287 | this.print(message);
288 | io.sockets.emit('log', message);
289 | this.emit('log', message);
290 | };
291 |
292 | // Print to stdout unless `--silent`
293 | BowerBrowser.prototype.print = function (data) {
294 | if (!this.options.silent) {
295 | process.stdout.write(data);
296 | }
297 | };
298 |
299 | // Stop server and wathcers
300 | BowerBrowser.prototype.close = function () {
301 | if (this.running) {
302 | if (gaze) {
303 | gaze.close();
304 | }
305 | if (io) {
306 | io.close();
307 | }
308 | this.running = false;
309 | }
310 | this.print('\nClosed bower-browser\n');
311 | this.emit('close');
312 | };
313 |
314 | // Module entry point
315 | module.exports = function (options) {
316 | return new BowerBrowser(options);
317 | };
318 |
--------------------------------------------------------------------------------
/client/assets/styles/app.scss:
--------------------------------------------------------------------------------
1 | @import url("//fonts.googleapis.com/css?family=Open+Sans:600,400");
2 |
3 | // Bower brand colors
4 | // http://bower.io/docs/about/#colors
5 | $bower-dark-brown: #543729;
6 | $bower-red: #ef5734;
7 | $bower-gold: #ffcc2f;
8 | $bower-green: #2baf2b;
9 | $bower-blue: #00acee;
10 | $bower-light-gray: #cecece;
11 |
12 | // Bootstrap config
13 | $gray-dark: $bower-dark-brown;
14 | $gray-light: #b2a49d;
15 | $brand-primary: $bower-red;
16 | $body-bg: #fcfcfc;
17 | $link-color: darken($bower-blue, 5%);
18 | $link-hover-color: darken($link-color, 10%);
19 | $font-family-sans-serif: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
20 | $font-size-base: 13px;
21 | $font-size-small: 12px;
22 | $font-size-h2: floor(($font-size-base * 2));
23 | $headings-font-weight: 600;
24 | $icon-font-path: "/assets/fonts/";
25 | $padding-base-vertical: 5px;
26 | $padding-base-horizontal: 10px;
27 | $padding-large-vertical: 6px;
28 | $padding-large-horizontal: 12px;
29 | $padding-small-vertical: 2px;
30 | $padding-small-horizontal: 7px;
31 | $padding-xs-vertical: 1px;
32 | $padding-xs-horizontal: 5px;
33 | $border-radius-base: 0;
34 | $border-radius-large: 0;
35 | $border-radius-small: 0;
36 | $btn-default-color: $bower-dark-brown;
37 | $btn-default-bg: #eaeaea;
38 | $input-bg-disabled: #f0f0f0;
39 | $input-color: $bower-dark-brown;
40 | $input-border-focus: darken($bower-gold, 10%);
41 | $input-color-placeholder: $gray-light;
42 | $cursor-disabled: default;
43 | $dropdown-link-hover-color: #fff;
44 | $dropdown-link-hover-bg: $bower-red;
45 | $pagination-color: $bower-dark-brown;
46 | $pagination-hover-color: $pagination-color;
47 | $tooltip-color: #f5e7e0;
48 | $tooltip-bg: $bower-dark-brown;
49 | $tooltip-opacity: 1;
50 | $popover-bg: $bower-gold;
51 | $popover-border-color: $bower-gold;
52 | $popover-fallback-border-color: darken($bower-gold, 10%);
53 | $close-text-shadow: none;
54 | $headings-small-color: #897266;
55 |
56 | @import "bootstrap-sass/assets/stylesheets/_bootstrap";
57 |
58 | // Application variables
59 | $navi-width: 220px;
60 | $content-min-width: $container-tablet;
61 | $content-max-width: $container-large-desktop - $navi-width;
62 | $console-height: 320px;
63 | $toggle-button-size: 30px;
64 |
65 | // Override and extend Bootstrap styles
66 | input[type="search"]::-webkit-search-cancel-button {
67 | -webkit-appearance: searchfield-cancel-button;
68 | }
69 |
70 | .form-control{
71 | transition: border-color ease-in-out 0.1s, box-shadow ease-in-out 0.1s;
72 | &:focus {
73 | border-color: darken($input-border-focus, 5%);
74 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 0 2px rgba($input-border-focus, 0.6);
75 | }
76 | &[readonly] {
77 | background-color: #fff;
78 | }
79 | }
80 |
81 | .control-label {
82 | display: block;
83 | }
84 |
85 | .btn {
86 | border-radius: 2px;
87 | transition-property: background-color;
88 | transition-duration: 0.1s;
89 | }
90 |
91 | .btn-default {
92 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);
93 | }
94 |
95 | .close {
96 | opacity: 0.3;
97 | &:hover,
98 | &:focus {
99 | opacity: 0.6;
100 | }
101 | }
102 |
103 | .table {
104 | table-layout: fixed;
105 | white-space: nowrap;
106 | th,
107 | td {
108 | overflow: hidden;
109 | text-overflow: ellipsis;
110 | }
111 | }
112 |
113 | .pagination {
114 | > li {
115 | > a,
116 | > span {
117 | margin-left: 3px;
118 | }
119 | }
120 | }
121 |
122 | .dropdown-menu {
123 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
124 | }
125 |
126 | .popover {
127 | font-size: $font-size-small;
128 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
129 | }
130 |
131 | // Components
132 | .toggle-button {
133 | display: block;
134 | width: $toggle-button-size;
135 | height: $toggle-button-size;
136 | line-height: $toggle-button-size;
137 | border-radius: 50%;
138 | text-align: center;
139 | background-color: $bower-gold;
140 | cursor: pointer;
141 | transform: translate(0, 0);
142 | transition-property: transform;
143 | transition-duration: 0.3s;
144 | transition-delay: 0.3s;
145 | .open & {
146 | transition-delay: 0s;
147 | }
148 | .glyphicon {
149 | color: lighten($bower-dark-brown, 15%);
150 | transition-property: color;
151 | transition-duration: 0.1s;
152 | }
153 | &:hover,
154 | &:focus {
155 | .glyphicon {
156 | color: $bower-dark-brown;
157 | }
158 | }
159 | }
160 |
161 | // Layout
162 | html {
163 | -ms-overflow-style: scrollbar;
164 | }
165 |
166 | .layout {
167 | padding-left: $navi-width;
168 | @media (max-width: $screen-sm-max) {
169 | padding-left: 0;
170 | }
171 | }
172 |
173 | .navigation {
174 | position: fixed;
175 | top: 0;
176 | left: $navi-width;
177 | bottom: 0;
178 | z-index: 10;
179 | width: $navi-width;
180 | margin-left: - $navi-width;
181 | background-color: $bower-gold;
182 | transition-property: left;
183 | transition-duration: 0.3s;
184 | @media (max-width: $screen-sm-max) {
185 | left: 0;
186 | &.open {
187 | left: $navi-width;
188 | box-shadow: 2px 0 4px rgba(0, 0, 0, 0.1);
189 | }
190 | }
191 | }
192 |
193 | .content {
194 | min-width: $content-min-width;
195 | max-width: $content-max-width;
196 | margin: 0 auto;
197 | padding: 30px $grid-gutter-width;
198 | }
199 |
200 | // Navigation
201 | .navigation {
202 | .toggle-button,
203 | .close {
204 | display: none;
205 | }
206 | }
207 | @media (max-width: $screen-sm-max) {
208 | .navigation {
209 | .toggle-button {
210 | display: block;
211 | position: fixed;
212 | top: 8px;
213 | left: 8px;
214 | }
215 | &.open .toggle-button {
216 | transform: translate(- ($toggle-button-size + 10px), 0);
217 | }
218 | .close {
219 | display: block;
220 | position: absolute;
221 | top: 8px;
222 | right: 12px;
223 | }
224 | }
225 | }
226 |
227 | .app-title {
228 | padding: 25px 20px;
229 | font-size: 20px;
230 | font-weight: 600;
231 | cursor: default;
232 | }
233 |
234 | .menu {
235 | margin-top: 0;
236 | margin-bottom: 0;
237 | padding-left: 0;
238 | list-style: none;
239 | > li > a {
240 | display: block;
241 | padding: 8px 20px;
242 | text-decoration: none;
243 | color: lighten($bower-dark-brown, 15%);
244 | transition-property: color, background-color;
245 | transition-duration: 0.1s;
246 | &:hover,
247 | &:focus {
248 | color: $bower-dark-brown;
249 | }
250 | }
251 | > li.active > a {
252 | color: #fff;
253 | background-color: $bower-red;
254 | }
255 | .glyphicon {
256 | margin-right: 6px;
257 | }
258 | }
259 |
260 | .menu-condensed {
261 | font-size: $font-size-small;
262 | > li > a {
263 | padding-top: 4px;
264 | padding-bottom: 4px;
265 | }
266 | }
267 |
268 | .menu-bottom {
269 | position: absolute;
270 | left: 0;
271 | right: 0;
272 | bottom: 40px;
273 | }
274 |
275 | // Content
276 | .page-title {
277 | margin-top: -5px;
278 | margin-bottom: 20px;
279 | color: $bower-green;
280 | small {
281 | color: lighten($bower-green, 15%);
282 | }
283 | }
284 |
285 | .message {
286 | margin-top: 50px;
287 | margin-bottom: 50px;
288 | text-align: center;
289 | p {
290 | margin-bottom: 25px;
291 | }
292 | }
293 |
294 | // Home
295 | .reload {
296 | .glyphicon {
297 | transition-property: transform;
298 | }
299 | &:active .glyphicon {
300 | transform: rotate(-360deg);
301 | transition-duration: 0s;
302 | }
303 | &:not(:active) .glyphicon {
304 | transition-duration: 0.5s;
305 | }
306 | }
307 |
308 | .home-action {
309 | margin-top: 35px;
310 | margin-bottom: 35px;
311 | .btn {
312 | margin-right: 5px;
313 | }
314 | }
315 |
316 | // Search
317 | .search-field {
318 | position: relative;
319 | margin-bottom: 25px;
320 | .form-control {
321 | padding-left: 32px;
322 | }
323 | .icon {
324 | position: absolute;
325 | top: 0;
326 | left: 0;
327 | width: $input-height-large;
328 | height: $input-height-large;
329 | line-height: $input-height-large;
330 | text-align: center;
331 | opacity: 0.7;
332 | pointer-events: none;
333 | }
334 | }
335 |
336 | .search-results {
337 | margin-top: 25px;
338 | margin-bottom: 25px;
339 | }
340 |
341 | .count {
342 | height: $input-height-base;
343 | line-height: $input-height-base;
344 | }
345 |
346 | .dropdown-check {
347 | .glyphicon {
348 | margin-left: -7px;
349 | margin-right: 8px;
350 | opacity: 0;
351 | }
352 | .checked .glyphicon {
353 | opacity: 1;
354 | }
355 | }
356 |
357 | // Search results
358 | .results {
359 | margin-top: 15px;
360 | margin-bottom: 15px;
361 | border-top: 1px solid $hr-border;
362 | }
363 |
364 | .result {
365 | padding-top: 15px;
366 | padding-bottom: 15px;
367 | border-bottom: 1px solid $hr-border;
368 | .title {
369 | margin-top: 2px;
370 | font-size: 22px;
371 | }
372 | .description {
373 | font-size: 14px;
374 | }
375 | .misc {
376 | font-size: $font-size-small;
377 | a {
378 | color: $gray-light;
379 | &:hover,
380 | &:focus {
381 | color: darken($gray-light, 15%);
382 | }
383 | }
384 | }
385 | .list-inline {
386 | margin-bottom: 3px;
387 | }
388 | }
389 |
390 | .result-action {
391 | $button-width: 90px;
392 | $dropdown-width: 18px;
393 | float: right;
394 | min-width: 260px;
395 | &.dependency {
396 | .form-control {
397 | padding-right: 90px;
398 | }
399 | }
400 | &.dev-dependency {
401 | .form-control {
402 | padding-right: 110px;
403 | }
404 | }
405 | .form-control {
406 | text-overflow: ellipsis;
407 | font-size: $font-size-small;
408 | }
409 | .form-control-feedback {
410 | right: 10px;
411 | width: auto;
412 | font-size: 11px;
413 | color: $bower-green;
414 | }
415 | .btn {
416 | height: $input-height-base;
417 | font-size: $font-size-small;
418 | }
419 | .btn-single,
420 | .btn-multiple {
421 | padding-left: 5px;
422 | padding-right: 5px;
423 | }
424 | .btn-single {
425 | min-width: $button-width;
426 | }
427 | .btn-multiple {
428 | min-width: $button-width - $dropdown-width + 1;
429 | }
430 | .btn.dropdown-toggle {
431 | width: $dropdown-width;
432 | padding-left: 0;
433 | padding-right: 0;
434 | }
435 | }
436 |
437 | .input-group-inner {
438 | display: table-cell;
439 | position: relative;
440 | }
441 |
442 | // Settings
443 | .settings-section {
444 | margin-bottom: 25px;
445 | .form-control {
446 | max-width: 320px;
447 | }
448 | }
449 |
450 | .tip-icon {
451 | top: 2px;
452 | margin-left: 5px;
453 | cursor: default;
454 | }
455 |
456 | // Console panel
457 | .console {
458 | .toggle-button {
459 | position: fixed;
460 | right: 8px;
461 | bottom: 8px;
462 | z-index: $zindex-navbar-fixed - 1;
463 | }
464 | &.open .toggle-button {
465 | transform: translate(0, $toggle-button-size + 10px);
466 | }
467 | }
468 |
469 | .console-panel {
470 | $color: #cfbdb5;
471 | position: fixed;
472 | left: 0;
473 | right: 0;
474 | bottom: 0;
475 | z-index: $zindex-navbar-fixed;
476 | overflow: hidden;
477 | color: $color;
478 | background-color: rgba(darken($bower-dark-brown, 12%), 0.9);
479 | transform: translate(0, 100%);
480 | transition-property: box-shadow, transform;
481 | transition-duration: 0.3s;
482 | ::selection {
483 | color: lighten($color, 15%);
484 | background-color: rgba(255, 255, 255, 0.18);
485 | }
486 | .open & {
487 | box-shadow: 0 -2px 4px rgba(0, 0, 0, 0.1);
488 | transform: translate(0, 0);
489 | }
490 | pre {
491 | color: inherit;
492 | }
493 | .close {
494 | position: absolute;
495 | top: 10px;
496 | right: 12px;
497 | color: $color;
498 | opacity: 0.7;
499 | &:hover,
500 | &:focus {
501 | opacity: 1;
502 | }
503 | }
504 | }
505 |
506 | .console-wrapper {
507 | overflow-y: scroll;
508 | height: $console-height;
509 | margin-right: -100px;
510 | padding-right: 100px;
511 | }
512 |
513 | .console-container {
514 | padding-left: $grid-gutter-width;
515 | padding-right: $grid-gutter-width;
516 | }
517 |
518 | .log {
519 | margin: 0;
520 | padding: 30px 0px;
521 | border: 0;
522 | border-radius: 0;
523 | white-space: pre;
524 | word-wrap: normal;
525 | background: none;
526 | }
527 |
--------------------------------------------------------------------------------