├── .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 |
2 |
3 |
4 |
5 |
{{process.log}}
6 |
7 |
8 | × 9 |
10 | 11 | 12 | 13 |
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 |
12 | 13 |
14 |
15 |
16 |
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 | 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 |
{{bower.path}}
4 |
5 |
6 |
7 |

dependencies

8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
NameVersion
{{name}}{{version}}
29 |
30 |
31 |

devDependencies

32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 |
NameVersion
{{name}}{{version}}
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 |
3 |

Search

4 |
5 | 6 |
7 |
8 | 9 |
10 |
11 | 12 |
13 |
14 | 15 |
16 |
17 | 18 |
19 |

Check at least one, or your search will not match any packages.

20 |
21 |
22 |
23 | 24 |
25 |
26 | 27 | 28 |
29 |
30 | 31 | 32 |
33 |
34 | 35 |
36 |
37 |
38 |
39 |
40 |

Bower

41 |
42 | 43 |
44 | 45 |
46 |
47 |
48 | 49 |
50 | 51 |
52 |
53 |
54 |
55 |

Reset settings

56 |
57 |

Restore all settings to their defaults.

58 |
59 | Reset settings 60 |
61 |
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 | ![bower-browser](resources/screenshot.png?raw=true) 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 | ![Features](resources/features.png?raw=true) 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 | 19 |
20 |
21 |
22 |
23 |
24 |
25 |

26 | {{::component.name}} 27 | by {{::component.owner}} 28 |

29 |

{{::component.description}}

30 |
31 | 36 |
    37 |
  • {{::component.stars | number}}
  • 38 |
  • Updated {{::component.updated | fromNow}}
  • 39 |
40 |
41 |
42 |
43 |
44 | 45 |
46 | 47 | 48 | 55 |
56 |
57 |
58 |
59 | 60 | 61 |
62 |
63 | 64 |
65 |
66 |
67 |
68 | 69 | 70 |
71 |
72 | 73 |
74 |
75 |
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 |
90 | 91 |
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 | --------------------------------------------------------------------------------