split dev server in two, preventing server source file from forcing webpack rebuilds

This commit is contained in:
Spencer Alger 2015-07-10 06:20:03 -07:00
parent 52570cbaad
commit 382598fada
47 changed files with 802 additions and 682 deletions

View file

@ -17,7 +17,6 @@
"angular-bootstrap": "0.10.0",
"angular-elastic": "2.4.2",
"angular-mocks": "1.2.28",
"angular-nvd3": "https://github.com/krispo/angular-nvd3.git#1.0.0-beta",
"angular-route": "1.2.28",
"angular-ui-ace": "0.2.3",
"bluebird": "~2.9.27",
@ -35,7 +34,6 @@
"ng-clip": "0.2.6",
"marked": "0.3.3",
"numeral": "1.5.3",
"nvd3": "1.7.1",
"leaflet-draw": "0.2.4"
},
"resolutions": {

View file

@ -39,6 +39,7 @@
"url": "https://github.com/elastic/kibana.git"
},
"dependencies": {
"angular-nvd3": "^1.0.0-beta",
"ansicolors": "^0.3.2",
"autoprefixer-loader": "^2.0.0",
"babel-jscs": "^1.0.3",
@ -47,10 +48,11 @@
"boom": "^2.8.0",
"bootstrap": "^3.3.5",
"bunyan": "^1.2.3",
"commander": "^2.6.0",
"commander": "^2.8.1",
"compression": "^1.3.0",
"cookie-parser": "^1.3.3",
"css-loader": "^0.15.1",
"d3": "^3.5.6",
"debug": "^2.1.1",
"elasticsearch": "^5.0.0",
"exports-loader": "^0.6.2",
@ -59,6 +61,7 @@
"extract-text-webpack-plugin": "^0.8.2",
"file-loader": "^0.8.4",
"font-awesome": "^4.3.0",
"gaze": "^0.5.1",
"glob": "^4.3.2",
"good": "^6.2.0",
"good-squeeze": "^2.1.0",
@ -77,6 +80,7 @@
"moment": "^2.10.3",
"node-libs-browser": "spalger/node-libs-browser",
"numeral": "^1.5.3",
"nvd3": "^1.8.1",
"raw-loader": "^0.5.1",
"request": "^2.40.0",
"requirefrom": "^0.2.0",

View file

@ -15,7 +15,7 @@ program
);
// attach commands
var serve = require('./commands/serve/serve')(program);
var serve = require('./commands/serve')(program);
// check for no command name
if (!argv[2] || argv[2][0] === '-') {

View file

@ -2,9 +2,9 @@
let _ = require('lodash');
let readYamlConfig = require('../../readYamlConfig');
let fromRoot = require('../../../utils/fromRoot');
let KbnServer = require('../../../server/KbnServer');
let readYamlConfig = require('../readYamlConfig');
let fromRoot = require('../../utils/fromRoot');
let KbnServer = require('../../server/KbnServer');
module.exports = function (program) {
program
@ -35,7 +35,7 @@ module.exports = function (program) {
if (opts.dev && !opts.noWatch && !require('cluster').isWorker) {
// stop processing the action and handoff to watch cluster manager
return require('./watch');
return require('../watch/watch');
}
let settings = readYamlConfig(opts.config || fromRoot('config/kibana.yml'));

View file

@ -1,88 +0,0 @@
'use strict';
let _ = require('lodash');
let Gaze = require('gaze').Gaze;
let join = require('path').join;
let cluster = require('cluster');
let ansicolors = require('ansicolors');
let cliPath = join(__dirname, '..', 'cli.js');
let green = require('../color').green;
let red = require('../color').red;
let yellow = require('../color').yellow;
console.log(yellow(' Kibana starting '), 'and watching for changes');
cluster.setupMaster({
exec: cliPath,
silent: false
});
let baseArgv = [process.execPath, cliPath].concat(_.difference(process.argv.slice(2), ['--no-watch']));
let serverArgv = JSON.stringify(baseArgv.concat(['--optimize.enable=false']));
let optimizerArgv = JSON.stringify(baseArgv.concat(['--plugins.initialize=false']));
let changedFiles = [];
let server;
let startServer = _.debounce(function () {
if (server && server.isDead()) {
server.kill(); // once "exit" event is received with 0 status, startServer() is called again
return;
}
server = cluster.fork({ kbnWorkerArgv: serverArgv });
server.on('online', function () {
if (!changedFiles.length) {
console.log(green(' Kibana Started '));
return;
}
let files = changedFiles.splice(0);
let prefix = files.length > 1 ? '\n - ' : '';
let fileList = files.reduce(function (list, file, i, files) {
return `${list || ''}${prefix}"${file}"`;
}, '');
console.log(yellow(' Kibana Restarted '), `due to changes in ${fileList}`);
});
}, 200);
let optimizer = cluster.fork({ kbnWorkerArgv: optimizerArgv });
optimizer.on('online', startServer);
cluster.on('exit', function (worker, code) {
if (worker === server) {
if (code > 0) {
console.log(red(' Kibana Crashed '), 'with status code', code);
} else {
// graceful shutdowns should only happen if we are restarting
startServer();
}
return;
}
if (worker === optimizer) {
console.log(red(' optimizer crashed '), 'with status code', code);
process.exit(1);
}
});
let gaze = new Gaze([
'src/**/*.{js,json,tmpl,yml}',
'!src/**/public/',
'config/',
], {
cwd: join(__dirname, '..', '..', '..')
});
// A file has been added/changed/deleted
gaze.on('all', function (event, path) {
changedFiles.push(path);
startServer();
});
gaze.on('error', function (err) {
console.log(red(' failed to watch files \n'), err.stack);
process.exit(1);
});

82
src/cli/watch/Worker.js Normal file
View file

@ -0,0 +1,82 @@
'use strict';
let _ = require('lodash');
let cluster = require('cluster');
let join = require('path').join;
let log = require('./log');
let cliPath = join(__dirname, '..', 'cli.js');
let baseArgs = _.difference(process.argv.slice(2), ['--no-watch']);
let baseArgv = [process.execPath, cliPath].concat(baseArgs);
cluster.setupMaster({
exec: cliPath,
silent: false
});
module.exports = class Worker {
constructor(gaze, opts) {
var self = this;
opts = opts || {};
self.type = opts.type;
self.title = opts.title || opts.type;
self.filter = opts.filter || _.constant(true);
self.changeBuffer = [];
self.env = {
kbnWorkerType: self.type,
kbnWorkerArgv: JSON.stringify(baseArgv.concat(opts.args || []))
};
self.start = _.debounce(_.bind(self.start, self), 25);
cluster.on('exit', function (fork, code) {
if (self.fork !== fork) return;
self.onExit(code);
});
}
onExit(code) {
if (code) log.red(`${this.title} crashed`, 'with status code', code);
else this.start(); // graceful shutdowns happen on restart
}
onChange(path) {
if (!this.filter(path)) return;
this.changeBuffer.push(path);
this.start();
}
onOnline() {
log.green(`${this.title} started`);
}
flushChangeBuffer() {
let files = _.unique(this.changeBuffer.splice(0));
let prefix = files.length > 1 ? '\n - ' : '';
return files.reduce(function (list, file, i, files) {
return `${list || ''}${prefix}"${file}"`;
}, '');
}
start() {
if (this.fork) {
if (!this.fork.isDead()) {
this.fork.kill();
// once "exit" event is received with 0 status, start() is called again
return;
}
if (this.changeBuffer.length) {
log.yellow(`${this.title} restarting`, `due to changes in ${this.flushChangeBuffer()}`);
}
}
else {
log.yellow(`${this.title} starting`);
}
this.fork = cluster.fork(this.env);
this.fork.once('online', _.bindKey(this, 'onOnline'));
}
};

12
src/cli/watch/log.js Normal file
View file

@ -0,0 +1,12 @@
'use strict';
let _ = require('lodash');
let ansicolors = require('ansicolors');
let log = _.restParam(function (color, label, rest1) {
console.log.apply(console, [color(` ${_.trim(label)} `)].concat(rest1));
});
exports.green = _.partial(log, require('../color').green);
exports.red = _.partial(log, require('../color').red);
exports.yellow = _.partial(log, require('../color').yellow);

48
src/cli/watch/watch.js Normal file
View file

@ -0,0 +1,48 @@
'use strict';
let _ = require('lodash');
let Gaze = require('gaze').Gaze;
let join = require('path').join;
let log = require('./log');
let cluster = require('cluster');
let Worker = require('./Worker');
let gaze = new Gaze([
'src/**/*.{js,yml}',
'!src/**/public/**/*',
'config/**/*',
], {
cwd: join(__dirname, '..', '..', '..'),
debounceDelay: 200
});
let workers = [
new Worker(gaze, {
type: 'optmzr',
title: 'optimizer',
args: ['--plugins.initialize=false', '--server.autoListen=false'],
filter: function (path) {
return /\/server\/optimize\//.test(path);
}
}),
new Worker(gaze, {
type: 'server',
args: ['--optimize.enable=false']
})
];
gaze.on('all', function (e, path) {
_.invoke(workers, 'onChange', path);
});
gaze.on('ready', function (watcher) {
log.green('Watching for changes', `(${_.size(watcher.watched())} files)`);
_.invoke(workers, 'start');
});
gaze.on('error', function (err) {
log.red('Failed to watch files!\n', err.stack);
process.exit(1);
});

3
src/plugins/.jshintrc Normal file
View file

@ -0,0 +1,3 @@
{
"extends": "../../.jshintrc.node"
}

View file

@ -0,0 +1,3 @@
{
"extends": "../../../../.jshintrc.browser"
}

View file

@ -1,35 +0,0 @@
.application {
background-color: #ecf0f1;
}
.app-links {
width: 700px;
margin: 25px auto;
text-align: justify;
}
.app-links .app-link {
display: inline-block;
background: white;
width: 200px;
margin: 0 30px 30px 0;
}
.app-links .app-link .app-icon {
display: block;
height: 200px;
background-size: cover;
background-position: center;
}
.app-links .app-link .app-info {
display: block;
padding: 15px;
}
.app-links .app-link .app-title {
margin: 0 0 10px;
color: #444444;
}
.app-links .app-link .app-description {
font-size: 1em;
color: #95a5a6;
text-align: left;
margin: 0;
}
/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi8vVXNlcnMvc3BlbmNlci9kZXYvZXMva2liYW5hL3NyYy91aS9hcHBTd2l0Y2hlci9hcHBTd2l0Y2hlci5sZXNzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUdBO0VBQ0UseUJBQUE7O0FBR0Y7RUFDRSxZQUFBO0VBQ0EsaUJBQUE7RUFDQSxtQkFBQTs7QUFIRixVQUtFO0VBQ0UscUJBQUE7RUFDQSxpQkFBQTtFQUNBLFlBQUE7RUFDQSxxQkFBQTs7QUFUSixVQUtFLFVBTUU7RUFDRSxjQUFBO0VBQ0EsYUFBQTtFQUNBLHNCQUFBO0VBQ0EsMkJBQUE7O0FBZk4sVUFLRSxVQWFFO0VBQ0UsY0FBQTtFQUNBLGFBQUE7O0FBcEJOLFVBS0UsVUFrQkU7RUFDRSxnQkFBQTtFQUNBLGNBQUE7O0FBekJOLFVBS0UsVUF1QkU7RUFDRSxjQUFBO0VBQ0EsY0FBQTtFQUNBLGdCQUFBO0VBQ0EsU0FBQSJ9 */

View file

@ -1,29 +1,33 @@
module.exports = function devServerPlugin(kibana) {
var _ = require('lodash');
var KbnServer = require('../server/KbnServer');
'use strict';
var glob = require('glob');
var join = require('path').join;
var basename = require('path').basename;
var relative = require('path').relative;
var normalize = require('path').normalize;
module.exports = function devModePlugin(kibana) {
let _ = require('lodash');
var ROOT = join(__dirname, '..', '..', '..');
var fromRoot = function () {
return normalize(join([ROOT].concat(_.toArray(arguments))));
};
let glob = require('glob');
let join = require('path').join;
let basename = require('path').basename;
let relative = require('path').relative;
let normalize = require('path').normalize;
let istanbul = require('./istanbul');
let amdWrapper = require('./amdWrapper');
let kibanaSrcFilter = require('./kibanaSrcFilter');
var SRC = fromRoot('src');
var NODE_MODULES = fromRoot('node_modules');
var UI = fromRoot('src/ui');
var TEST = fromRoot('test');
var UNIT = fromRoot('test/unit');
var SPECS = fromRoot('test/unit/specs/**/*.js');
var istanbul = require('./lib/istanbul');
var amdWrapper = require('./lib/amd_wrapper');
var kibanaSrcFilter = require('./lib/kibana_src_filter');
let fromRoot = require('../../utils/fromRoot');
const ROOT = fromRoot('.');
const SRC = fromRoot('src');
const NODE_MODULES = fromRoot('node_modules');
const UI = fromRoot('src/ui');
const TEST = fromRoot('test');
const UNIT = fromRoot('test/unit');
const SPECS = fromRoot('test/unit/specs/**/*.js');
return new kibana.Plugin({
initCondition: function (config) {
return config.get('env.dev');
},
init: function (server, options) {
server.ext('onPreHandler', istanbul({ root: SRC, displayRoot: SRC, filter: kibanaSrcFilter }));
server.ext('onPreHandler', istanbul({ root: UI, displayRoot: SRC, filter: kibanaSrcFilter }));

View file

@ -1,4 +1,5 @@
{
"name": "devMode",
"main": "./devMode.js",
"version": "1.0.0"
}

View file

@ -7,6 +7,7 @@ module.exports = function (kibana) {
config: function (Joi) {
return Joi.object({
enabled: Joi.boolean().default(true),
url: Joi.string().uri({ scheme: ['http', 'https'] }).default('http://localhost:9200'),
preserveHost: Joi.boolean().default(true),
username: Joi.string(),

View file

@ -6,6 +6,8 @@ module.exports = function (kibana) {
config: function (Joi) {
return Joi.object({
enabled: Joi.boolean().default(true),
defaultAppId: Joi.string().default('discover'),
index: Joi.string().default('.kibana'),
buildNum: Joi.string().default('@@buildNum')
}).default();

View file

@ -0,0 +1,41 @@
module.exports = function (kibana) {
return new kibana.Plugin({
init: function (server, options) {
var app = this.app;
server.expose('app', app);
server.route({
path: '/status',
method: 'GET',
handler: function (req, reply) {
return reply.renderApp(app);
}
});
},
uiExports: {
app: {
title: 'Server Status',
main: 'plugins/serverStatus/serverStatus',
hidden: false,
defaultModules: {
angular: [],
require: [
'chrome',
'angular-bootstrap'
]
.concat(kibana.autoload.styles)
}
},
loaders: [
{ test: /\/angular-nvd3\//, loader: 'imports?angular,d3' }
]
}
});
};

View file

@ -0,0 +1,4 @@
{
"name": "serverStatus",
"version": "1.0.0"
}

View file

@ -0,0 +1,3 @@
{
"extends": "../../../../.jshintrc.browser"
}

View file

@ -0,0 +1,218 @@
var angular = require('angular');
var $ = require('jquery');
var _ = require('lodash');
var moment = require('moment');
var numeral = require('numeral');
require('angular-nvd3');
// Make sure we don't have to deal with statuses by hand
function getStatus(plugin) {
var statusMap = {
green: {
label: 'success',
msg: 'Ready',
idx: 1
},
yellow: {
label: 'warning',
msg: 'S.N.A.F.U.',
idx: 2
},
red: {
label: 'danger',
msg: 'Danger Will Robinson! Danger!',
idx: 3
},
loading: {
label: 'info',
msg: 'Loading...',
idx: 0
}
};
if (!_.isObject(plugin) || _.isUndefined(plugin)) {
plugin = {state: plugin};
}
return statusMap[plugin.state];
}
function getLabel(plugin) { return getStatus(plugin).label; }
// Turns thisIsASentence to
// This Is A Sentence
function niceName(name) {
return name
.split(/(?=[A-Z])/)
.map(function (word) { return word[0].toUpperCase() + _.rest(word).join(''); })
.join(' ');
}
function formatNumber(num, which) {
var format = '0.00';
var postfix = '';
switch (which) {
case 'time':
return moment(num).format('HH:mm:ss');
case 'byte':
format += 'b';
break;
case 'ms':
postfix = 'ms';
break;
}
return numeral(num).format(format) + postfix;
}
function numberType(key) {
var byteMetrics = ['heapTotal', 'heapUsed', 'rss'];
var msMetrics = ['delay', 'responseTimeAvg', 'responseTimeMax'];
var preciseMetric = ['requests', 'load'];
if ( byteMetrics.indexOf(key) > -1 ) {
return 'byte';
} else if (msMetrics.indexOf(key) > -1 ) {
return 'ms';
} else {
return 'precise';
}
}
var makeChartOptions = _.memoize(function (type) {
return {
chart: {
type: 'lineChart',
height: 200,
showLegend: false,
showXAxis: false,
showYAxis: false,
useInteractiveGuideline: true,
tooltips: true,
pointSize: 0,
color: ['#444', '#777', '#aaa'],
margin: {
top: 10,
left: 0,
right: 0,
bottom: 20
},
xAxis: { tickFormat: function (d) { return formatNumber(d, 'time'); } },
yAxis: { tickFormat: function (d) { return formatNumber(d, type); }, },
y: function (d) { return d.y; },
x: function (d) { return d.x; }
}
};
});
// The Kibana App
require('modules')
.get('KibanaStatusApp', ['nvd3'])
.controller('StatusPage', function ($scope, $http, $window, $timeout) {
// the object representing all of the elements the ui touches
$scope.ui = {
// show the system status by going through all of the plugins,
// and making sure they're green.
systemStatus: (function () {
// for convenience
function getIdx(plugin) { return getStatus(plugin).idx; }
return function () {
var currentStatus = 'loading';
var currentIdx = getIdx(currentStatus);
// FIXME eh, not too thrilled about this.
var status = _.reduce($scope.ui.plugins, function (curr, plugin, key) {
var pluginIdx = getIdx(plugin);
if (pluginIdx > currentIdx) {
// set the current status
currentStatus = plugin.state;
currentIdx = getIdx(plugin);
}
return currentStatus;
}, 'loading');
// give the ui the label for colors and such
return getStatus(status);
};
}()),
charts: {},
plugins: []
};
var windowHasFocus = true;
angular.element($window).bind({
blur: function () { windowHasFocus = false; },
focus: function () {
windowHasFocus = true;
getAppStatus();
}
});
function getAppStatus() {
// go ahead and get the info you want
$http
.get('/status/health')
.success(function (data) {
// Assign the propper variables to the scope and change them as necessary
// setup The charts
// wrap the metrics data and append the average
$scope.ui.charts = _.mapValues(data.metrics, function (metric, name) {
// Metric Values format
// metric: [[xValue, yValue], ...]
// LoadMetric:
// metric: [[xValue, [yValue, yValue2, yValue3]], ...]
// return [
// {type: 'line', key: name, yAxis: 1, values: [{x: xValue, y: yValue}, ...]},
// {type: 'line', key: name, yAxis: 1, values: [{x: xValue, y: yValue1}, ...]},
// {type: 'line', key: name, yAxis: 1, values: [{x: xValue, y: yValue2}, ...]}]
//
// Go through all of the metric values and split the values out.
// returns an array of all of the averages
var metricList = [];
var metricNumberType = numberType(name);
// convert the [x,y] into {x: x, y: y}
metric.forEach(function (vector) {
vector = _.flatten(vector);
var x = vector.shift();
vector.forEach(function (yValue, idx) {
if (!metricList[idx]) {
metricList[idx] = {
key: name + idx,
values: []
};
}
// unshift to make sure they're in the correct order
metricList[idx].values.unshift({x: x, y: yValue});
});
});
var average = metricList.map(function (data) {
var uglySum = data.values.reduce(function (sumSoFar, vector) {
return sumSoFar + vector.y;
}, 0);
return formatNumber(uglySum / data.values.length, metricNumberType);
});
var options = makeChartOptions(metricNumberType);
return { data: metricList, average: average, niceName: niceName(name), options: options };
});
// give the plugins their proper name so CSS classes can be properply applied
$scope.ui.plugins = _.mapValues(data.status, function (plugin) {
plugin.uiStatus = getLabel(plugin);
return plugin;
});
if (windowHasFocus) {
// go ahead and get another status in 5 seconds
$timeout(getAppStatus, 5000);
}
})
.error(function () {
window.alert('Something went terribly wrong while making the request!!! Perhaps your server is down?');
});
}
// Start it all up
getAppStatus();
});

View file

@ -0,0 +1,41 @@
<header>
<h1>
<strong>Kibana</strong>&nbsp;Status Page
</h1>
</header>
<div>
<section class="section">
<h4>What is this page?</h4>
<p>This page is your sanity check, and your savior. You can check for potential problems</p>
<p>Here is the status of your kibana instance and the plugins you have installed along with some, statistics to asses potential problems.</p>
</section>
<div class="system_status_wrapper system_status_{{ui.systemStatus().label}}">
<h3 class="title">
<b>System Status:</b> {{ui.systemStatus().msg}}
</h3>
<div id="plugin_table">
<div class="plugin_table_header plugin_row">
<div class="col-md-1 plugin_key">Plugin</div>
<div class="col-md-11 plugin_state">Status</div>
<div class="clearfix"></div>
</div>
<div ng-repeat="(key, plugin) in ui.plugins" class="plugin_table_plugin plugin_row plugin_status_{{plugin.uiStatus}}">
<div class="col-md-1 plugin_key">{{key}}</div>
<div class="col-md-11 plugin_state">{{plugin.message}}</div>
<div class="clearfix"></div>
</div>
</div>
</div>
<h2>Server Metrics</h2>
<p>Interval of 5 seconds, with a max history of 5 minutes.</p>
<div id="chart_cont" class="row">
<div ng-repeat="(key, chart) in ui.charts" class="status_chart_wrapper col-md-4">
<h3 class="title">{{chart.niceName}}</h2>
<h4 class="average">
<span ng-repeat="average in chart.average track by $index"><span ng-if="$index">, </span>{{average}}</span>
</h4>
<nvd3 options="chart.options" data="chart.data"></nvd3>
</div>
<div class="clearfix"></div>
</div>
</div>

View file

@ -0,0 +1,13 @@
require('plugins/serverStatus/KibanaStatusApp');
require('plugins/serverStatus/serverStatus.less');
require('chrome')
.setNavBackground('grey')
.setTabs([
{
id: '',
title: 'Server Status'
}
])
.setRootTemplate(require('plugins/serverStatus/serverStatus.html'))
.setRootController('statusPage', 'StatusPage');

View file

@ -1,88 +1,94 @@
var _ = require('lodash');
var Promise = require('bluebird');
var Hapi = require('hapi');
var dirname = require('path').dirname;
'use strict';
var rootDir = require('../utils/fromRoot')('.');
var package = require('../utils/closestPackageJson').getSync();
let _ = require('lodash');
let EventEmitter = require('events').EventEmitter;
let promify = require('bluebird').promisify;
let each = require('bluebird').each;
let Hapi = require('hapi');
function KbnServer(settings) {
this.name = package.name;
this.version = package.version;
this.build = package.build || false;
this.rootDir = rootDir;
this.settings = settings || {};
let rootDir = require('../utils/fromRoot')('.');
let pkg = require('../utils/closestPackageJson').getSync();
this.server = new Hapi.Server();
module.exports = class KbnServer extends EventEmitter {
constructor(settings) {
super();
// mixin server modules
this.ready = _.constant(this.mixin(
require('./config'),
require('./logging'),
require('./http'),
require('./ui'), // sets this.uiExports
require('./status'), // sets this.status
require('./plugins'), // sets this.plugins
require('./optimize')
));
}
this.name = pkg.name;
this.version = pkg.version;
this.build = pkg.build || false;
this.rootDir = rootDir;
this.settings = settings || {};
/**
* Extend the KbnServer outside of the constraits of a plugin. This allows access
* to APIs that are not exposed (intentionally) to the plugins and should only
* be used when the code will be kept up to date with Kibana.
*
* @param {...function} - functions that should be called to mixin functionality.
* They are called with the arguments (kibana, server, config)
* and can return a promise to delay execution of the next mixin
* @return {Promise} - promise that is resolved when the final mixin completes.
*/
KbnServer.prototype.mixin = function (/* ...fns */) {
var fns = _.toArray(arguments);
var self = this;
var server = self.server;
this.server = new Hapi.Server();
return Promise.each(fns, function (fn) {
return fn(self, server, server.config && server.config());
})
// clear the return value
.then(_.noop);
};
require('./config')(this, this.server);
let config = this.server.config();
/**
* Tell the server to listen for incoming requests.
*
* @return {Promise} resolved with the server once it is listening
*/
KbnServer.prototype.listen = function () {
var self = this;
var server = self.server;
var start = _.ary(Promise.promisify(server.start, server), 0);
this.ready = _.constant(this.mixin(
require('./logging'),
require('./http'),
require('./status'), // sets this.status
require('./plugins'), // sets this.plugins
require('./ui'), // sets this.uiExports
require('./optimize')
));
return self.ready()
.then(function () {
return self.mixin(start, require('./pid'));
})
.then(
function () {
server.log('info', 'Server running at ' + server.info.uri);
return server;
},
function (err) {
server.log('fatal', err);
throw err;
this.listen = _.once(this.listen);
if (config.get('server.autoListen')) {
this.listen();
}
);
};
}
// if this module was called from the command line, go ahead and start listening
if (require.main === module) {
(new KbnServer())
.listen()
.catch(function (err) {
console.log(err.stack);
process.exit(1);
});
} else {
module.exports = KbnServer;
}
/**
* Extend the KbnServer outside of the constraits of a plugin. This allows access
* to APIs that are not exposed (intentionally) to the plugins and should only
* be used when the code will be kept up to date with Kibana.
*
* @param {...function} - functions that should be called to mixin functionality.
* They are called with the arguments (kibana, server, config)
* and can return a promise to delay execution of the next mixin
* @return {Promise} - promise that is resolved when the final mixin completes.
*/
mixin(/* ...fns */) {
let fns = _.compact(_.toArray(arguments));
let self = this;
let server = self.server;
return each(fns, function (fn) {
return fn.call(self, self, server, server.config());
})
.then(_.noop); // clear the return value
}
/**
* Tell the server to listen for incoming requests, or get
* a promise that will be resolved once the server is listening.
*
* Calling this function has no effect, unless the "server.autoListen"
* is set to false.
*
* @return undefined
*/
listen() {
let self = this;
let server = self.server;
let start = _.ary(promify(server.start, server), 0);
self.ready().then(function () {
return self.mixin(start, require('./pid'));
})
.then(
function () {
server.log('info', 'Server running at ' + server.info.uri);
self.emit('listening');
return server;
},
function (err) {
server.log('fatal', err);
self.emit('error', err);
}
);
return this;
}
};

View file

@ -1,104 +1,114 @@
var Promise = require('bluebird');
var Joi = require('joi');
var _ = require('lodash');
var override = require('./override');
'use strict';
function Config(schema, defaults) {
this.schema = schema || Joi.object({}).default();
this.config = {};
this.set(defaults);
}
let Promise = require('bluebird');
let Joi = require('joi');
let _ = require('lodash');
let override = require('./override');
Config.prototype.extendSchema = function (key, schema) {
var additionalSchema = {};
if (!this.has(key)) {
additionalSchema[key] = schema;
this.schema = this.schema.keys(additionalSchema);
this.reset(this.config);
}
};
Config.prototype.reset = function (obj) {
this._commit(obj);
};
Config.prototype.set = function (key, value) {
// clone and modify the config
var config = _.cloneDeep(this.config);
if (_.isPlainObject(key)) {
config = override(config, key);
} else {
_.set(config, key, value);
module.exports = class Config {
constructor(schema, defaults) {
this.schema = schema || Joi.object({}).default();
this.config = {};
this.set(defaults);
}
// attempt to validate the config value
this._commit(config);
};
Config.prototype._commit = function (newConfig) {
// resolve the current environment
var env = newConfig.env;
delete newConfig.env;
if (_.isObject(env)) env = env.name;
if (!env) env = process.env.NODE_ENV || 'production';
// pass the environment as context so that it can be refed in config
var context = {
env: env,
prod: env === 'production',
dev: env === 'development',
};
if (!context.dev && !context.prod) {
throw new TypeError(`Unexpected environment "${env}", expected one of "development" or "production"`);
}
var results = Joi.validate(newConfig, this.schema, {
context: context
});
if (results.error) {
throw results.error;
}
this.config = results.value;
};
Config.prototype.get = function (key) {
if (!key) {
return _.cloneDeep(this.config);
}
var value = _.get(this.config, key);
if (value === undefined) {
extendSchema(key, schema) {
let additionalSchema = {};
if (!this.has(key)) {
throw new Error('Unknown config key: ' + key);
additionalSchema[key] = schema;
this.schema = this.schema.keys(additionalSchema);
this.reset(this.config);
} else {
throw new Error(`Config schema already has key ${key}`);
}
}
return _.cloneDeep(value);
};
Config.prototype.has = function (key) {
function has(key, schema, path) {
path = path || [];
// Catch the partial paths
if (path.join('.') === key) return true;
// Only go deep on inner objects with children
if (schema._inner.children.length) {
for (var i = 0; i < schema._inner.children.length; i++) {
var child = schema._inner.children[i];
// If the child is an object recurse through it's children and return
// true if there's a match
if (child.schema._type === 'object') {
if (has(key, child.schema, path.concat([child.key]))) return true;
// if the child matches, return true
} else if (path.concat([child.key]).join('.') === key) {
return true;
reset(obj) {
this._commit(obj);
}
set(key, value) {
// clone and modify the config
let config = _.cloneDeep(this.config);
if (_.isPlainObject(key)) {
config = override(config, key);
} else {
_.set(config, key, value);
}
// attempt to validate the config value
this._commit(config);
}
_commit(newConfig) {
// resolve the current environment
let env = newConfig.env;
delete newConfig.env;
if (_.isObject(env)) env = env.name;
if (!env) env = process.env.NODE_ENV || 'production';
// pass the environment as context so that it can be refed in config
let context = {
env: env,
prod: env === 'production',
dev: env === 'development',
};
if (!context.dev && !context.prod) {
throw new TypeError(`Unexpected environment "${env}", expected one of "development" or "production"`);
}
let results = Joi.validate(newConfig, this.schema, {
context: context
});
if (results.error) {
throw results.error;
}
this.config = results.value;
}
get(key) {
if (!key) {
return _.cloneDeep(this.config);
}
let value = _.get(this.config, key);
if (value === undefined) {
if (!this.has(key)) {
throw new Error('Unknown config key: ' + key);
}
}
return _.cloneDeep(value);
}
has(key) {
function has(key, schema, path) {
path = path || [];
// Catch the partial paths
if (path.join('.') === key) return true;
// Only go deep on inner objects with children
if (schema._inner.children.length) {
for (let i = 0; i < schema._inner.children.length; i++) {
let child = schema._inner.children[i];
// If the child is an object recurse through it's children and return
// true if there's a match
if (child.schema._type === 'object') {
if (has(key, child.schema, path.concat([child.key]))) return true;
// if the child matches, return true
} else if (path.concat([child.key]).join('.') === key) {
return true;
}
}
}
}
}
return !!has(key, this.schema);
};
module.exports = Config;
if (_.isArray(key)) {
// TODO: add .has() support for array keys
key = key.join('.');
}
return !!has(key, this.schema);
}
};

View file

@ -19,6 +19,7 @@ module.exports = Joi.object({
server: Joi.object({
host: Joi.string().hostname().default('0.0.0.0'),
port: Joi.number().default(5601),
autoListen: Joi.boolean().default(true),
defaultRoute: Joi.string(),
ssl: Joi.object({
cert: Joi.string(),
@ -52,10 +53,12 @@ module.exports = Joi.object({
plugins: Joi.object({
paths: Joi.array().items(Joi.string()).default([]),
scanDirs: Joi.array().items(Joi.string()).default([])
scanDirs: Joi.array().items(Joi.string()).default([]),
initialize: Joi.boolean().default(true)
}).default(),
optimize: Joi.object({
enable: Joi.boolean().default(true),
bundleDir: Joi.string().default(fromRoot('optimize/bundles')),
viewCaching: Joi.boolean().default(Joi.ref('$prod')),
watch: Joi.boolean().default(Joi.ref('$dev')),

View file

@ -16,16 +16,23 @@ let typeColors = {
error: 'red',
fatal: 'magenta',
plugins: 'yellow',
debug: 'brightBlack'
debug: 'brightBlack',
server: 'brightBlack',
optmzr: 'white',
};
let color = _.memoize(function (name) {
return ansicolors[typeColors[name]] || _.identity;
});
let type = _.memoize(function (t) {
return _.chain(t).padLeft(6).trunc(6).thru(color(t)).value();
});
let workerType = process.env.kbnWorkerType ? `${type(process.env.kbnWorkerType)}: ` : '';
module.exports = class KbnLoggerJsonFormat extends LogFormat {
format(data) {
let type = _.chain(data.type).padLeft(6).trunc(6).thru(color(data.type)).value();
let time = color('time')(moment(data.timestamp).format());
let msg = data.error ? color('error')(data.error.stack) : color('message')(data.message);
@ -37,6 +44,6 @@ module.exports = class KbnLoggerJsonFormat extends LogFormat {
return s + `[${ color(t)(t) }]`;
}, '');
return `${type}: [${time}] ${tags} ${msg}`;
return `${workerType}${type(data.type)}: [${time}] ${tags} ${msg}`;
}
};

View file

@ -72,7 +72,6 @@ class Optimizer extends EventEmitter {
resolve: {
extensions: ['', '.js', '.less'],
packageMains: [],
modulesDirectories: [ fromRoot('node_modules'), assets.root ],
root: fromRoot(),
alias: modules.aliases

View file

@ -8,9 +8,10 @@ module.exports = function (kbnServer, server, config) {
var status = kbnServer.status.create('optimize');
server.exposeStaticDir('/bundles/{path*}', bundleDir);
if (config.get('optimize.sourceMaps')) {
server.exposeStaticDir('/src/{path*}', fromRoot('src'));
server.exposeStaticDir('/node_modules/{paths*}', fromRoot('node_modules'));
if (!config.get('optimize.enable')) {
status.disabled();
return;
}
return (new Optimizer({

View file

@ -6,6 +6,10 @@ let Joi = require('joi');
let Promise = require('bluebird');
let join = require('path').join;
const defaultConfigSchema = Joi.object({
enabled: Joi.boolean().default(true)
}).default();
module.exports = class Plugin {
constructor(kbnServer, path, pkg, opts) {
this.kbnServer = kbnServer;
@ -17,8 +21,9 @@ module.exports = class Plugin {
this.requiredIds = opts.require || [];
this.version = opts.version || pkg.version;
this.publicDir = _.get(opts, 'publicDir', join(path, 'public'));
this.externalCondition = opts.initCondition || _.constant(true);
this.externalInit = opts.init || _.noop;
this.getConfig = opts.config || _.noop;
this.getConfigSchema = opts.config || _.noop;
this.init = _.once(this.init);
}
@ -45,18 +50,28 @@ module.exports = class Plugin {
});
return Promise.try(function () {
return self.getConfig(Joi);
return self.getConfigSchema(Joi);
})
.then(function (schema) {
if (schema) config.extendSchema(id, schema);
else config.extendSchema(id, defaultConfigSchema);
})
.then(function () {
return status.decoratePlugin(self);
})
.then(function () {
return self.kbnServer.uiExports.consumePlugin(self);
if (config.get([id, 'enabled'])) {
return self.externalCondition(config);
}
})
.then(function () {
.then(function (enabled) {
if (!enabled) {
// Only change the plugin status if it wasn't set by the externalCondition
if (self.status.state === 'uninitialized') {
self.status.disabled();
}
return;
}
let register = function (server, options, next) {
Promise.try(self.externalInit, [server, options], self).nodeify(next);
@ -69,15 +84,14 @@ module.exports = class Plugin {
register: register,
options: config.has(id) ? config.get(id) : null
}, cb);
})
.then(function () {
// Only change the plugin status to green if the
// intial status has not been updated
if (self.status.state === 'uninitialized') {
self.status.green('Ready');
}
});
})
.then(function () {
// Only change the plugin status to green if the
// intial status has not been updated
if (self.status.state === 'uninitialized') {
self.status.green('Ready');
}
});
}

View file

@ -11,7 +11,7 @@ module.exports = class Plugins extends Array {
this.kbnServer = kbnServer;
}
load(path) {
new(path) {
var self = this;
var api = new PluginApi(this.kbnServer, path);

View file

@ -9,9 +9,14 @@ module.exports = function (kbnServer, server, config) {
return resolve(kbnServer.pluginPaths)
.map(function (path) {
return plugins.load(path);
return plugins.new(path);
})
.then(function () {
if (!config.get('plugins.initialize')) {
server.log(['info'], 'Plugin initialization disabled.');
return;
}
var others = _.indexBy(plugins, 'id');
return Promise.all(plugins.map(function recurse(plugin) {

View file

@ -1,3 +0,0 @@
{
"extends": "../../../../../.jshintrc.browser"
}

View file

@ -1,229 +0,0 @@
define(function (require) {
var angular = require('angular');
var $ = require('jquery');
var _ = require('lodash');
var moment = require('moment');
var numeral = require('numeral');
require('nvd3_directives');
// Make sure we don't have to deal with statuses by hand
function getStatus(plugin) {
var statusMap = {
green: {
label: 'success',
msg: 'Ready',
idx: 1
},
yellow: {
label: 'warning',
msg: 'S.N.A.F.U.',
idx: 2
},
red: {
label: 'danger',
msg: 'Danger Will Robinson! Danger!',
idx: 3
},
loading: {
label: 'info',
msg: 'Loading...',
idx: 0
}
};
if (!_.isObject(plugin) || _.isUndefined(plugin)) {
plugin = {state: plugin};
}
return statusMap[plugin.state];
}
function getLabel(plugin) { return getStatus(plugin).label; }
// Turns thisIsASentence to
// This Is A Sentence
function niceName(name) {
return name
.split(/(?=[A-Z])/)
.map(function (word) { return word[0].toUpperCase() + _.rest(word).join(''); })
.join(' ');
}
function formatNumber(num, which) {
var format = '0.00';
var postfix = '';
switch (which) {
case 'time':
return moment(num).format('HH:mm:ss');
case 'byte':
format += 'b';
break;
case 'ms':
postfix = 'ms';
break;
}
return numeral(num).format(format) + postfix;
}
function numberType(key) {
var byteMetrics = ['heapTotal', 'heapUsed', 'rss'];
var msMetrics = ['delay', 'responseTimeAvg', 'responseTimeMax'];
var preciseMetric = ['requests', 'load'];
if ( byteMetrics.indexOf(key) > -1 ) {
return 'byte';
} else if (msMetrics.indexOf(key) > -1 ) {
return 'ms';
} else {
return 'precise';
}
}
var makeChartOptions = _.memoize(function (type) {
return {
chart: {
type: 'lineChart',
height: 200,
showLegend: false,
showXAxis: false,
showYAxis: false,
useInteractiveGuideline: true,
tooltips: true,
pointSize: 0,
color: ['#444', '#777', '#aaa'],
margin: {
top: 10,
left: 0,
right: 0,
bottom: 20
},
xAxis: { tickFormat: function (d) { return formatNumber(d, 'time'); } },
yAxis: { tickFormat: function (d) { return formatNumber(d, type); }, },
y: function (d) { return d.y; },
x: function (d) { return d.x; }
}
};
});
// The Kibana App
require('modules')
.get('KibanaStatusApp', ['nvd3'])
.controller('StatusPage', function ($scope, $http, $window, $timeout) {
// the object representing all of the elements the ui touches
$scope.ui = {
// show the system status by going through all of the plugins,
// and making sure they're green.
systemStatus: (function () {
// for convenience
function getIdx(plugin) { return getStatus(plugin).idx; }
return function () {
var currentStatus = 'loading';
var currentIdx = getIdx(currentStatus);
// FIXME eh, not too thrilled about this.
var status = _.reduce($scope.ui.plugins, function (curr, plugin, key) {
var pluginIdx = getIdx(plugin);
if (pluginIdx > currentIdx) {
// set the current status
currentStatus = plugin.state;
currentIdx = getIdx(plugin);
}
return currentStatus;
}, 'loading');
// give the ui the label for colors and such
return getStatus(status);
};
}()),
charts: {},
plugins: []
};
var windowHasFocus = true;
angular.element($window).bind({
blur: function () { windowHasFocus = false; },
focus: function () {
windowHasFocus = true;
getAppStatus();
}
});
function getAppStatus() {
// go ahead and get the info you want
$http
.get('/status/health')
.success(function (data) {
// Assign the propper variables to the scope and change them as necessary
// setup The charts
// wrap the metrics data and append the average
$scope.ui.charts = _.mapValues(data.metrics, function (metric, name) {
// Metric Values format
// metric: [[xValue, yValue], ...]
// LoadMetric:
// metric: [[xValue, [yValue, yValue2, yValue3]], ...]
// return [
// {type: 'line', key: name, yAxis: 1, values: [{x: xValue, y: yValue}, ...]},
// {type: 'line', key: name, yAxis: 1, values: [{x: xValue, y: yValue1}, ...]},
// {type: 'line', key: name, yAxis: 1, values: [{x: xValue, y: yValue2}, ...]}]
//
// Go through all of the metric values and split the values out.
// returns an array of all of the averages
var metricList = [];
var metricNumberType = numberType(name);
// convert the [x,y] into {x: x, y: y}
metric.forEach(function (vector) {
vector = _.flatten(vector);
var x = vector.shift();
vector.forEach(function (yValue, idx) {
if (!metricList[idx]) {
metricList[idx] = {
key: name + idx,
values: []
};
}
// unshift to make sure they're in the correct order
metricList[idx].values.unshift({x: x, y: yValue});
});
});
var average = metricList.map(function (data) {
var uglySum = data.values.reduce(function (sumSoFar, vector) {
return sumSoFar + vector.y;
}, 0);
return formatNumber(uglySum / data.values.length, metricNumberType);
});
var options = makeChartOptions(metricNumberType);
return { data: metricList, average: average, niceName: niceName(name), options: options };
});
// give the plugins their proper name so CSS classes can be properply applied
$scope.ui.plugins = _.mapValues(data.status, function (plugin) {
plugin.uiStatus = getLabel(plugin);
return plugin;
});
if (windowHasFocus) {
// go ahead and get another status in 5 seconds
$timeout(getAppStatus, 5000);
}
})
.error(function () {
window.alert('Something went terribly wrong while making the request!!! Perhaps your server is down?');
});
}
// Start it all up
getAppStatus();
});
return {
init: function () {
$(function () {
angular.bootstrap(window.document, ['nvd3', 'KibanaStatusApp']);
});
}
};
});

View file

@ -1,23 +0,0 @@
require.config({
baseUrl: '/',
paths: {
angular: '/bower_components/angular/angular',
css: '/bower_components/require-css/css',
d3: '/bower_components/d3/d3',
jquery: '/bower_components/jquery/dist/jquery',
lodash: '/utils/lodash-mixins/index',
lodash_src: '/bower_components/lodash/lodash',
moment: '/bower_components/moment/moment',
nvd3: '/bower_components/nvd3/build/nv.d3',
nvd3_directives: '/bower_components/angular-nvd3/dist/angular-nvd3',
numeral: '/bower_components/numeral/numeral'
},
shim: {
angular: {
deps: ['jquery'],
exports: 'angular'
},
nvd3: ['css!bower_components/nvd3/build/nv.d3.css', 'd3'],
nvd3_directives: ['angular', 'nvd3']
}
});

View file

@ -1,20 +1,33 @@
var Status = require('./Status');
'use strict';
function ServerStatus(server) {
this.server = server;
this.each = {};
}
let _ = require('lodash');
ServerStatus.prototype.create = function (name) {
return (this.each[name] = new Status(name, this.server));
let Status = require('./Status');
module.exports = class ServerStatus {
constructor(server) {
this.server = server;
this._created = {};
}
create(name) {
return (this._created[name] = new Status(name, this.server));
}
each(fn) {
let self = this;
_.forOwn(self._created, function (status, i, list) {
if (status.state !== 'disabled') {
fn.call(self, status, i, list);
}
});
}
decoratePlugin(plugin) {
plugin.status = this.create(plugin.id);
}
toJSON() {
return this._created;
}
};
ServerStatus.prototype.decoratePlugin = function (plugin) {
plugin.status = this.create(plugin.id);
};
ServerStatus.prototype.toJSON = function () {
return this.each;
};
module.exports = ServerStatus;

View file

@ -1,3 +1,4 @@
var _ = require('lodash');
var util = require('util');
var EventEmitter = require('events').EventEmitter;
@ -11,7 +12,7 @@ function Status(name, server) {
this.on('change', function (current, previous) {
server.log(['plugins', name, 'info'], {
tmpl: 'Change status from <%= prev %> to <%= cur %> - <%= curMsg %>',
tmpl: 'Status changed from <%= prev %> to <%= cur %><% curMsg && print(` - ${curMsg}`) %>',
name: name,
prev: previous.state,
cur: current.state,
@ -23,6 +24,13 @@ function Status(name, server) {
Status.prototype.green = makeStatusUpdateFn('green');
Status.prototype.yellow = makeStatusUpdateFn('yellow');
Status.prototype.red = makeStatusUpdateFn('red');
Status.prototype.disabled = _.wrap(makeStatusUpdateFn('disabled'), function (update, msg) {
var ret = update.call(this, msg);
this.green = this.yellow = this.red = _.noop;
return ret;
});
function makeStatusUpdateFn(color) {
return function (message) {
var previous = {

View file

@ -10,8 +10,6 @@ module.exports = function (kbnServer) {
kbnServer.status = new ServerStatus(kbnServer.server);
kbnServer.metrics = new Samples(60);
server.exposeStaticDir('/status/{path*}', join(__dirname, 'public'));
server.plugins.good.monitor.on('ops', function (event) {
var port = config.get('server.port');

View file

@ -5,12 +5,11 @@ var join = require('path').join;
var defAutoload = require('./autoload');
class UiApp {
constructor(uiExports, plugin, spec) {
constructor(uiExports, spec) {
this.uiExports = uiExports;
this.plugin = plugin || null;
this.spec = spec || {};
this.id = this.spec.id || _.get(this, 'plugin.id');
this.id = this.spec.id;
if (!this.id) {
throw new Error('Every app must specify it\'s id');
}

View file

@ -19,12 +19,12 @@ module.exports = class UiApps extends Array {
}
new(plugin, spec) {
new(spec) {
if (this.hidden && spec.hidden) {
return this.hidden.new(plugin, spec);
return this.hidden.new(spec);
}
let app = new UiApp(this.uiExports, plugin, spec);
let app = new UiApp(this.uiExports, spec);
if (_.includes(this.claimedIds, app.id)) {
throw new Error('Unable to create two apps with the id ' + app.id + '.');
@ -34,6 +34,7 @@ module.exports = class UiApps extends Array {
this._byId = null;
this.push(app);
return app;
}
get byId() {

View file

@ -9,9 +9,10 @@ class UiExports {
constructor(kbnServer) {
this.kbnServer = kbnServer;
this.apps = new UiApps(this);
this.appCount = 0;
this.aliases = {};
this.exportConsumer = _.memoize(this.exportConsumer);
kbnServer.plugins.forEach(_.bindKey(this, 'consumePlugin'));
}
consumePlugin(plugin) {
@ -35,7 +36,8 @@ class UiExports {
switch (type) {
case 'app':
return function (plugin, spec) {
self.apps.new(plugin, spec);
spec = _.defaults({}, spec, { id: plugin.id });
plugin.app = self.apps.new(spec);
};
case 'visTypes':

View file

@ -42,7 +42,8 @@ module.exports = function (grunt) {
'amdefine@0.1.1': ['BSD-3-Clause', 'MIT'],
'flatten@0.0.1': ['MIT'],
'color-name@1.0.0': ['UNLICENSE'],
'css-color-names@0.0.1': ['MIT']
'css-color-names@0.0.1': ['MIT'],
'inherits@1.0.0': ['ISC']
}
}
};

View file

@ -1,34 +0,0 @@
module.exports = function (grunt) {
var _ = require('lodash');
grunt.registerTask('dev', function () {
var tasks = [
'less:dev',
'jade',
'esvm:dev',
'maybeStartKibana',
'watch'
];
if (!grunt.option('with-es')) {
_.pull(tasks, 'esvm:dev');
}
grunt.task.run(tasks);
});
grunt.registerTask('devServer', function (keepalive) {
var port = grunt.option('port');
var quiet = !(grunt.option('debug') || grunt.option('verbose'));
require('../src/devServer').run(port, quiet)
.then(function (server) {
grunt.log.ok('Server started: ' + server.info.uri);
if (keepalive) {
// return a never resolving promise
return new Promise(_.noop);
}
})
.nodeify(this.async());
});
};

View file

@ -36,17 +36,19 @@ module.exports = function (grunt) {
var licenseStats = _.map(result, processPackage);
var invalidLicenses = _.filter(licenseStats, function (pkg) { return !pkg.valid;});
if (grunt.option('only-invalid')) {
grunt.log.debug(JSON.stringify(invalidLicenses, null, 2));
} else {
if (!grunt.option('only-invalid')) {
grunt.log.debug(JSON.stringify(licenseStats, null, 2));
}
if (invalidLicenses.length) {
grunt.fail.warn('Non-confirming licenses: ' + _.pluck(invalidLicenses, 'name').join(', ') +
'. Use --only-invalid for details.', invalidLicenses.length);
grunt.log.debug(JSON.stringify(invalidLicenses, null, 2));
grunt.fail.warn(
'Non-confirming licenses: ' + _.pluck(invalidLicenses, 'name').join(', '),
invalidLicenses.length
);
}
done();
}
}

View file

@ -1,15 +0,0 @@
module.exports = function (grunt) {
var join = require('path').join;
grunt.registerTask('require_css_deps:copy', function () {
[
'css-builder.js',
'normalize.js'
].forEach(function (dep) {
grunt.file.copy(
join(grunt.config.get('bowerComponentsDir'), 'require-css', dep),
join(grunt.config.get('build'), 'src', dep)
);
});
});
};