Move from watching optimizer to live optimizer

This commit is contained in:
spalger 2015-08-06 23:04:01 -07:00
parent 1f95e662df
commit 70ef0125b9
37 changed files with 883 additions and 904 deletions

View file

@ -17,7 +17,7 @@ module.exports = function (config) {
// list of files / patterns to load in the browser
files: [
'http://localhost:5601/bundles/tests.bundle.js',
'http://localhost:5601/bundles/tests.bundle.style.css'
'http://localhost:5601/bundles/tests.style.css'
],
proxies: {

View file

@ -0,0 +1,86 @@
let cluster = require('cluster');
let { join } = require('path');
let { compact, invoke, bindAll, once } = require('lodash');
let Log = require('../Log');
let Worker = require('./Worker');
module.exports = class ClusterManager {
constructor(opts) {
this.log = new Log(opts.quiet, opts.silent);
this.addedCount = 0;
this.workers = [
new Worker({
type: 'optmzr',
title: 'optimizer',
log: this.log,
argv: compact([
(opts.quiet || opts.silent || opts.verbose) ? null : '--quiet',
'--plugins.initialize=false',
'--server.autoListen=false'
]),
watch: false
}),
new Worker({
type: 'server',
log: this.log
})
];
bindAll(this, 'onWatcherAdd', 'onWatcherError', 'onWatcherChange');
if (opts.watch) this.setupWatching();
else this.startCluster();
}
startCluster() {
invoke(this.workers, 'start');
}
setupWatching() {
var chokidar = require('chokidar');
let utils = require('requirefrom')('src/utils');
let fromRoot = utils('fromRoot');
this.watcher = chokidar.watch([
'src/cli',
'src/optimize',
'src/plugins',
'src/server',
'src/ui',
'src/utils',
'config',
'installedPlugins'
], {
cwd: fromRoot('.'),
ignored: /[\\\/](node_modules|bower_components|public)[\\\/]/,
});
this.watcher.on('add', this.onWatcherAdd);
this.watcher.on('error', this.onWatcherError);
this.watcher.on('ready', once(() => {
// start sending changes to workers
this.watcher.removeListener('add', this.onWatcherAdd);
this.watcher.on('all', this.onWatcherChange);
this.log.good('Watching for changes', `(${this.addedCount} files)`);
this.startCluster();
}));
}
onWatcherAdd() {
this.addedCount += 1;
}
onWatcherChange(e, path) {
invoke(this.workers, 'onChange', path);
}
onWatcherError(err) {
this.log.bad('Failed to watch files!\n', err.stack);
process.exit(1); // eslint-disable-line no-process-exit
}
};

View file

@ -22,7 +22,7 @@ module.exports = class Worker extends EventEmitter {
this.log = opts.log;
this.type = opts.type;
this.title = opts.title || opts.type;
this.filters = opts.filters;
this.watch = (opts.watch !== false);
this.changes = [];
let argv = _.union(baseArgv, opts.argv || []);
@ -53,15 +53,7 @@ module.exports = class Worker extends EventEmitter {
}
onChange(path) {
var valid = true;
if (this.filters) {
valid = _.any(this.filters, function (filter) {
return filter.test(path);
});
}
if (!valid) return;
if (!this.watch) return;
this.changes.push(path);
this.start();
}

View file

@ -3,10 +3,8 @@ let { isWorker } = require('cluster');
let { resolve } = require('path');
let cwd = process.cwd();
let readYamlConfig = require('./readYamlConfig');
let src = require('requirefrom')('src');
let fromRoot = src('utils/fromRoot');
let KbnServer = src('server/KbnServer');
let pathCollector = function () {
let paths = [];
@ -52,19 +50,25 @@ module.exports = function (program) {
.option('--plugins <path>', 'an alias for --plugin-dir', pluginDirCollector)
.option('--dev', 'Run the server with development mode defaults')
.option('--no-watch', 'Prevent watching, use with --dev to prevent server restarts')
.option('--no-lazy', 'Prevent lazy optimization of applications, only works with --dev')
.action(function (opts) {
if (opts.dev && opts.watch && !isWorker) {
// stop processing the action and handoff to watch cluster manager
return require('../watch/watch')(opts);
if (opts.dev && !isWorker) {
// stop processing the action and handoff to cluster manager
let ClusterManager = require('../cluster/ClusterManager');
new ClusterManager(opts);
return;
}
let readYamlConfig = require('./readYamlConfig');
let KbnServer = src('server/KbnServer');
let settings = readYamlConfig(opts.config || fromRoot('config/kibana.yml'));
let set = _.partial(_.set, settings);
let get = _.partial(_.get, settings);
if (opts.dev) {
set('env', 'development');
set('optimize.watch', opts.watch);
set('optimize.lazy', opts.lazy);
}
if (opts.elasticsearch) set('elasticsearch.url', opts.elasticsearch);

View file

@ -1,90 +0,0 @@
let cluster = require('cluster');
let { join } = require('path');
let _ = require('lodash');
var chokidar = require('chokidar');
let utils = require('requirefrom')('src/utils');
let fromRoot = utils('fromRoot');
let Log = require('../Log');
let Worker = require('./Worker');
module.exports = function (opts) {
let watcher = chokidar.watch([
'src/cli',
'src/fixtures',
'src/server',
'src/utils',
'src/plugins',
'config',
], {
cwd: fromRoot('.'),
ignore: 'src/plugins/*/public/**/*'
});
let log = new Log(opts.quiet, opts.silent);
let customLogging = opts.quiet || opts.silent || opts.verbose;
let workers = [
new Worker({
type: 'optmzr',
title: 'optimizer',
log: log,
argv: _.compact([
customLogging ? null : '--quiet',
'--plugins.initialize=false',
'--server.autoListen=false',
'--optimize._workerRole=send'
]),
filters: [
/src\/server\/(optimize|ui|plugins)\//,
/src\/plugins\/[^\/]+\/[^\/]+\.js$/,
/src\/cli\//
]
}),
new Worker({
type: 'server',
log: log,
argv: [
'--optimize._workerRole=receive'
]
})
];
workers.forEach(function (worker) {
worker.on('broadcast', function (msg) {
workers.forEach(function (to) {
if (to !== worker && to.fork) to.fork.send(msg);
});
});
});
var addedCount = 0;
function onAddBeforeReady() {
addedCount += 1;
}
function onReady() {
// start sending changes to workers
watcher.removeListener('add', onAddBeforeReady);
watcher.on('all', onAnyChangeAfterReady);
log.good('Watching for changes', `(${addedCount} files)`);
_.invoke(workers, 'start');
}
function onAnyChangeAfterReady(e, path) {
_.invoke(workers, 'onChange', path);
}
function onError(err) {
log.bad('Failed to watch files!\n', err.stack);
process.exit(1); // eslint-disable-line no-process-exit
}
watcher.on('add', onAddBeforeReady);
watcher.on('ready', onReady);
watcher.on('error', onError);
};

View file

@ -1,4 +1,3 @@
let { EventEmitter } = require('events');
let { inherits } = require('util');
let _ = require('lodash');
let { join } = require('path');
@ -9,24 +8,17 @@ let ExtractTextPlugin = require('extract-text-webpack-plugin');
let utils = require('requirefrom')('src/utils');
let fromRoot = utils('fromRoot');
let OptmzBundles = require('./OptmzBundles');
let OptmzUiModules = require('./OptmzUiModules');
let babelOptions = require('./babelOptions');
let kbnTag = `Kibana ${ utils('packageJson').version }`;
class BaseOptimizer extends EventEmitter {
class BaseOptimizer {
constructor(opts) {
super();
this.env = opts.env;
this.bundles = opts.bundles;
this.sourceMaps = opts.sourceMaps || false;
this.modules = new OptmzUiModules(opts.plugins);
this.bundles = new OptmzBundles(
opts,
`${kbnTag} ${this.constructor.name} ${ this.sourceMaps ? ' (with source maps)' : ''}`
);
}
_.bindAll(this, 'getConfig');
async initCompiler() {
return this.compiler || (this.compiler = webpack(this.getConfig()));
}
getConfig() {
@ -34,12 +26,12 @@ class BaseOptimizer extends EventEmitter {
return {
context: fromRoot('.'),
entry: this.bundles.getEntriesConfig(),
entry: this.bundles.toWebpackEntries(),
devtool: this.sourceMaps ? '#source-map' : false,
output: {
path: this.bundles.dir,
path: this.env.workingDir,
filename: '[name].bundle.js',
sourceMapFilename: '[file].map',
publicPath: '/bundles/',
@ -87,8 +79,8 @@ class BaseOptimizer extends EventEmitter {
nonStandard: true
}, babelOptions)
}
].concat(this.modules.loaders),
noParse: this.modules.noParse,
].concat(this.env.loaders),
noParse: this.env.noParse,
},
resolve: {
@ -97,7 +89,7 @@ class BaseOptimizer extends EventEmitter {
modulesDirectories: ['node_modules'],
loaderPostfixes: ['-loader', ''],
root: fromRoot('.'),
alias: this.modules.aliases
alias: this.env.aliases
}
};
}

View file

@ -1,47 +0,0 @@
let _ = require('lodash');
let webpack = require('webpack');
let BaseOptimizer = require('./BaseOptimizer');
module.exports = class CachedOptimizer extends BaseOptimizer {
constructor(opts) {
super(opts);
_.bindAll(this, 'init', 'setupCompiler', 'run');
}
init(autoRun) {
return this.bundles.ensureAllEntriesExist().then(autoRun ? this.run : this.setupCompiler);
}
setupCompiler(autoRun) {
this.entries = this.bundles.getMissingEntries();
if (!this.entries.length) return;
this.compilerConfig = this.getConfig();
this.compiler = webpack(this.compilerConfig);
if (autoRun) this.run();
}
run() {
if (!this.compiler) {
return this.setupCompiler(true);
}
var self = this;
let entries = self.entries;
self.emit('build-start', entries);
self.compiler.run(function (err, stats) {
if (err) {
self.emit('error', entries, stats, err);
}
else if (stats.hasErrors() || stats.hasWarnings()) {
self.emit('error', entries, stats, new Error('Optimization must not produce errors or warnings'));
}
else {
self.emit('done', entries, stats);
}
});
}
};

View file

@ -0,0 +1,27 @@
let { fromNode } = require('bluebird');
let BaseOptimizer = require('./BaseOptimizer');
module.exports = class FsOptimizer extends BaseOptimizer {
async init() {
await this.bundles.writeEntryFiles();
await this.bundles.filterCachedBundles();
}
async run() {
await this.initCompiler();
await fromNode(cb => {
this.compiler.run((err, stats) => {
if (err || !stats) return cb(err);
if (stats.hasErrors() || stats.hasWarnings()) {
let err = new Error('Optimization must not produce errors or warnings');
err.stats = stats;
return cb(err);
}
else {
cb(null, stats);
}
});
});
}
};

View file

@ -1,53 +1,102 @@
let _ = require('lodash');
let { contains } = require('lodash');
let { join } = require('path');
let { promisify } = require('bluebird');
let { fromNode } = require('bluebird');
let webpack = require('webpack');
let Boom = require('boom');
let MemoryFileSystem = require('memory-fs');
let BaseOptimizer = require('./BaseOptimizer');
module.exports = class LiveOptimizer extends BaseOptimizer {
constructor(opts) {
super(opts);
this.compilerConfig = this.getConfig();
// this.compilerConfig.profile = true;
this.compiler = webpack(this.compilerConfig);
async init() {
await this.bundles.writeEntryFiles();
await this.initCompiler();
this.outFs = this.compiler.outputFileSystem = new MemoryFileSystem();
_.bindAll(this, 'get', 'init');
this.compile = promisify(this.compiler.run, this.compiler);
}
init() {
return this.bundles.ensureAllEntriesExist(true);
}
async compile() {
let stats = await fromNode(cb => this.compiler.run(cb));
get(id) {
let self = this;
let fs = self.outFs;
let filename = join(self.compiler.outputPath, `${id}.bundle.js`);
let mapFilename = join(self.compiler.outputPath, `${id}.bundle.js.map`);
let styleFilename = join(self.compiler.outputPath, `${id}.style.css`);
if (!self.active) {
self.active = self.compile().finally(function () {
self.active = null;
});
if (stats.hasErrors() || stats.hasWarnings()) {
let err = new Error('optimization failure');
err.stats = stats;
throw err;
}
return self.active.then(function (stats) {
if (stats.hasErrors() || stats.hasWarnings()) {
console.log(stats.toString({ colors: true }));
return null;
return stats;
}
async start() {
let prom = null;
try {
prom = this.current = this.compile();
return await prom;
}
catch (e) {
if (e.stats && this.current === prom) {
console.log(e.stats.toString({ colors: true }));
}
return {
bundle: fs.readFileSync(filename),
sourceMap: self.sourceMaps ? fs.readFileSync(mapFilename) : false,
style: fs.readFileSync(styleFilename)
};
throw e;
}
finally {
this.current = null;
}
}
/**
* Read a file from the in-memory bundles, paths just like those
* produces by the FsOptimizer are used to access files. The first time
* a file is requested it is marked as "sent". If that same file is requested
* a second time then we will rerun the compiler.
*
* !!!ONLY ONE BROWSER TAB SHOULD ACCESS THE IN-MEMORY OPTIMIZERS FILES AT A TIME!!!
*
* @param {[type]} relativePath [description]
* @return {[type]} [description]
*/
async get(relativePath) {
try {
let fs = this.outFs;
let path = join(this.compiler.outputPath, relativePath);
let restart = contains(this.sent, path);
if (!this.sent || restart) {
this.sent = [];
this.current = null;
}
await (this.current || this.start());
let content = fs.readFileSync(path); // may throw "Path doesn't exist"
this.sent.push(path);
return content || '';
}
catch (error) {
if (error && error.message.match(/Path doesn't exist/)) {
error = Boom.notFound();
}
console.log(error.stack);
throw error;
}
}
bindToServer(server) {
server.route({
path: '/bundles/{path*}',
method: 'GET',
handler: async (request, reply) => {
let path = request.params.path;
let mimeType = server.mime.path(path).type;
try {
let content = await this.get(path);
return reply(content).type(mimeType);
} catch (error) {
return reply(Boom.wrap(error, 500));
}
}
});
}
};

View file

@ -1,111 +0,0 @@
let _ = require('lodash');
let { join } = require('path');
let { resolve, promisify } = require('bluebird');
let rimraf = promisify(require('rimraf'));
let mkdirp = promisify(require('mkdirp'));
let stat = promisify(require('fs').stat);
let read = promisify(require('fs').readFile);
let write = promisify(require('fs').writeFile);
let unlink = promisify(require('fs').unlink);
let readdir = promisify(require('fs').readdir);
let readSync = require('fs').readFileSync;
let entryFileTemplate = _.template(readSync(join(__dirname, 'entry.js.tmpl')));
class OptmzBundles {
constructor(opts, optimizerTagline) {
this.dir = _.get(opts, 'bundleDir');
if (!_.isString(this.dir)) {
throw new TypeError('Optimizer requires a working directory');
}
this.sourceMaps = _.get(opts, 'sourceMaps');
this.entries = _.get(opts, 'entries', []).map(function (spec) {
let entry = {
id: spec.id,
deps: _.get(spec, 'deps', []),
modules: _.get(spec, 'modules', []),
path: join(this.dir, spec.id + '.entry.js'),
bundlePath: join(this.dir, spec.id + '.bundle.js'),
exists: undefined,
content: undefined
};
entry.content = _.get(spec, 'template', entryFileTemplate)({
entry: entry,
optimizerTagline: optimizerTagline
});
return entry;
}, this);
_.bindAll(this, [
'ensureDir',
'ensureAllEntriesExist',
'checkIfEntryExists',
'writeEntryFile',
'clean',
'getMissingEntries',
'getEntriesConfig'
]);
}
ensureDir() {
return mkdirp(this.dir);
}
ensureAllEntriesExist(overwrite) {
return this.ensureDir()
.return(this.entries)
.map(overwrite ? this.checkIfEntryExists : _.noop)
.return(this.entries)
.map(this.writeEntryFile)
.return(undefined);
}
checkIfEntryExists(entry) {
return resolve([
read(entry.path),
stat(entry.bundlePath)
])
.settle()
.spread(function (readEntry, statBundle) {
let existingEntry = readEntry.isFulfilled() && readEntry.value().toString('utf8');
let bundleExists = statBundle.isFulfilled() && !statBundle.value().isDirectory();
entry.exists = existingEntry && bundleExists && (existingEntry === entry.content);
});
}
writeEntryFile(entry) {
return this.clean([entry.path, entry.bundlePath]).then(function () {
entry.exists = false;
return write(entry.path, entry.content, { encoding: 'utf8' });
});
}
// unlinks files, swallows missing file errors
clean(paths) {
return resolve(
_.flatten([paths]).map(function (path) {
return rimraf(path);
})
)
.settle()
.return(undefined);
}
getMissingEntries() {
return _.reject(this.entries, 'exists');
}
getEntriesConfig() {
return _.transform(this.getMissingEntries(), function (map, entry) {
map[entry.id] = entry.path;
}, {});
}
}
module.exports = OptmzBundles;

View file

@ -1,98 +0,0 @@
var _ = require('lodash');
var fromRoot = require('../utils/fromRoot');
var asRegExp = _.flow(
_.escapeRegExp,
function (path) {
return path + '(?:\\.js)?$';
},
RegExp
);
function OptmzUiExports(plugins) {
// regular expressions which will prevent webpack from parsing the file
var noParse = this.noParse = [];
// webpack aliases, like require paths, mapping a prefix to a directory
var aliases = this.aliases = {
ui: fromRoot('src/ui/public'),
testHarness: fromRoot('src/testHarness/public')
};
// webpack loaders map loader configuration to regexps
var loaders = this.loaders = [];
var claimedModuleIds = {};
_.each(plugins, function (plugin) {
var exports = plugin.uiExportsSpecs;
// add an alias for this plugins public directory
if (plugin.publicDir) {
aliases[`plugins/${plugin.id}`] = plugin.publicDir;
}
// consume the plugin's "modules" exports
_.forOwn(exports.modules, function (spec, id) {
if (claimedModuleIds[id]) {
throw new TypeError(`Plugin ${plugin.id} attempted to override export "${id}" from ${claimedModuleIds[id]}`);
} else {
claimedModuleIds[id] = plugin.id;
}
// configurable via spec
var path;
var parse = true;
var imports = null;
var exports = null;
var expose = null;
// basic style, just a path
if (_.isString(spec)) path = spec;
if (_.isArray(spec)) {
path = spec[0];
imports = spec[1];
exports = spec[2];
}
if (_.isPlainObject(spec)) {
path = spec.path;
parse = _.get(spec, 'parse', parse);
imports = _.get(spec, 'imports', imports);
exports = _.get(spec, 'exports', exports);
expose = _.get(spec, 'expose', expose);
}
if (!path) {
throw new TypeError('Invalid spec definition, unable to identify path');
}
aliases[id] = path;
var loader = [];
if (imports) {
loader.push(`imports?${imports}`);
}
if (exports) loader.push(`exports?${exports}`);
if (expose) loader.push(`expose?${expose}`);
if (loader.length) loaders.push({ test: asRegExp(path), loader: loader.join('!') });
if (!parse) noParse.push(asRegExp(path));
});
// consume the plugin's "loaders" exports
_.each(exports.loaders, function (loader) {
loaders.push(loader);
});
// consume the plugin's "noParse" exports
_.each(exports.noParse, function (regExp) {
noParse.push(regExp);
});
});
}
module.exports = OptmzUiExports;

View file

@ -1,118 +0,0 @@
let _ = require('lodash');
let webpack = require('webpack');
let BaseOptimizer = require('./BaseOptimizer');
const STATUS_BUNDLE_INVALID = 'bundle invalid';
const STATUS_BUNDLING = 'optimizing';
const STATUS_REBUNDLING = 'bundle invalid during optimizing';
const STATUS_ERROR = 'error';
const STATUS_DONE = 'done';
class WatchingOptimizer extends BaseOptimizer {
constructor(opts) {
super(opts);
this.bundleStatus = null;
_.bindAll(this, 'init', 'setupCompiler', 'onBundlesInvalid', 'setStatus', 'enable', 'disable');
this.run = this.enable; // enable makes a bit more sense here, but alias for consistency with CachedOptimizer
}
init(autoEnable) {
return this.bundles.ensureAllEntriesExist(true).then(autoEnable ? this.enable : this.setupCompiler);
}
setupCompiler(autoEnable) {
if (!_.size(this.bundles.entries)) return;
this.compilerConfig = this.getConfig();
this.compiler = webpack(this.compilerConfig);
this.compiler.plugin('watch-run', _.partial(this.setStatus, STATUS_BUNDLING));
this.compiler.plugin('invalid', this.onBundlesInvalid);
this.compiler.plugin('failed', _.partial(this.setStatus, STATUS_ERROR));
this.compiler.plugin('done', _.partial(this.setStatus, STATUS_DONE));
if (autoEnable) this.enable();
}
onBundlesInvalid() {
switch (this.bundleStatus || null) {
case STATUS_BUNDLING:
case STATUS_REBUNDLING:
// if the source changed during building, we immediately rebuild
return this.setStatus(STATUS_REBUNDLING);
case null:
// the bundle has to be something before that something can be invalid
return;
default:
return this.setStatus(STATUS_BUNDLE_INVALID);
}
}
setStatus(status) {
let self = this;
let entries = self.bundles.entries;
let stats;
let error;
let shouldBeFinal = false;
switch (status) {
case 'done':
stats = self.watcher.stats;
error = null;
shouldBeFinal = true;
if (stats.hasErrors()) {
error = new Error('Optimization must not produce errors or warnings');
status = 'error';
}
break;
case 'error':
stats = self.watcher.stats;
error = self.watcher.error;
}
let apply = function () {
clearTimeout(self.tentativeStatusChange);
self.tentativeStatusChange = null;
self.emit(self.bundleStatus = status, entries, stats, error);
};
if (shouldBeFinal) {
// this looks race-y, but it's how webpack does it: https://goo.gl/ShVo2o
self.tentativeStatusChange = setTimeout(apply, 0);
} else {
apply();
}
// webpack allows some plugins to be async, we don't want to hold up webpack,
// so just always callback if we get a cb();
let cb = _.last(arguments);
if (typeof cb === 'function') cb();
}
enable() {
if (!this.compiler) {
return this.setupCompiler(true);
}
if (this.watcher) {
throw new Error('WatchingOptimizer already watching!');
}
this.watcher = this.compiler.watch({}, _.noop);
}
disable() {
if (!this.compiler) return;
if (!this.watcher) return;
this.watcher.close();
this.watcher = null;
this.compiler = null;
}
}
module.exports = WatchingOptimizer;

View file

@ -1,37 +1,36 @@
let _ = require('lodash');
let { once, template } = require('lodash');
let { resolve } = require('path');
let { readFileSync } = require('fs');
let src = require('requirefrom')('src');
let fromRoot = src('utils/fromRoot');
let pathContains = src('utils/pathContains');
let LiveOptimizer = src('optimize/LiveOptimizer');
let id = 'tests';
let globAll = require('./globAll');
let testEntryFileTemplate = _.template(readFileSync(resolve(__dirname, './testBundleEntry.js.tmpl')));
let testEntryFileTemplate = template(readFileSync(resolve(__dirname, './testBundleEntry.js.tmpl')));
class TestBundler {
constructor(kbnServer) {
this.kbnServer = kbnServer;
this.init = _.once(this.init);
_.bindAll(this, ['init', 'findTestFiles', 'setupOptimizer', 'render']);
this.hapi = kbnServer.server;
this.plugins = kbnServer.plugins;
this.bundleDir = kbnServer.config.get('optimize.bundleDir');
this.init = once(this.init);
}
init() {
return this.findTestFiles().then(this.setupOptimizer);
async init() {
await this.findTestFiles();
await this.setupOptimizer();
}
findTestFiles() {
return globAll(fromRoot('src'), [
'**/public/**/__tests__/**/*.js'
async findTestFiles() {
await globAll(fromRoot('.'), [
'src/**/public/**/__tests__/**/*.js',
'installedPlugins/**/public/**/__tests__/**/*.js'
]);
}
setupOptimizer(testFiles) {
let plugins = this.kbnServer.plugins;
let bundleDir = this.kbnServer.config.get('optimize.bundleDir');
async setupOptimizer(testFiles) {
let deps = [];
let modules = [];
@ -39,7 +38,7 @@ class TestBundler {
modules = modules.concat(testFiles);
}
plugins.forEach(function (plugin) {
this.plugins.forEach(function (plugin) {
if (!plugin.app) return;
modules = modules.concat(plugin.app.getModules());
deps = deps.concat(plugin.app.getRelatedPlugins());
@ -47,7 +46,7 @@ class TestBundler {
this.optimizer = new LiveOptimizer({
sourceMaps: true,
bundleDir: bundleDir,
bundleDir: this.bundleDir,
entries: [
{
id: id,
@ -56,22 +55,12 @@ class TestBundler {
template: testEntryFileTemplate
}
],
plugins: plugins
plugins: this.plugins
});
return this.optimizer.init();
}
await this.optimizer.init();
render() {
let self = this;
let first = !this.optimizer;
let server = this.kbnServer.server;
return self.init()
.then(function () {
server.log(['optimize', 'testHarness', first ? 'info' : 'debug'], 'Test harness built, compiling test bundle');
return self.optimizer.get(id);
});
this.optimizer.bindToServer(this.hapi);
}
}

39
src/optimize/dist/dist.js vendored Normal file
View file

@ -0,0 +1,39 @@
/**
* Optimize source code in a way that is suitable for distribution
*/
module.exports = async (kbnServer, server, config) => {
let FsOptimizer = require('../FsOptimizer');
let optimizer = new FsOptimizer({
env: kbnServer.bundles.env,
bundles: kbnServer.bundles,
sourceMaps: config.get('optimize.sourceMaps')
});
server.exposeStaticDir('/bundles/{path*}', kbnServer.bundles.env.workingDir);
try {
await optimizer.init();
let bundleIds = optimizer.bundles.getIds();
if (bundleIds.length) {
server.log(
['warning', 'optimize'],
`Optimizing bundles for ${bundleIds.join(', ')}. This may take a few minutes.`
);
} else {
server.log(
['debug', 'optimize'],
`All bundles are cached and ready to go!`
);
}
await optimizer.run();
} catch (e) {
if (e.stats) {
server.log(['error'], e.stats.toString({ colors: true }));
}
server.log(['fatal'], e);
process.exit(1); // eslint-disable-line no-process-exit
}
};

View file

@ -1,20 +0,0 @@
/**
* Optimized application entry file
*
* This is programatically created and updated, do not modify
*
* built using: <%= optimizerTagline %>
* includes code from:
<%
entry.deps.sort().forEach(function (plugin) {
print(` * - ${plugin}\n`);
})
%> *
*/
require('ui/chrome');
<%
entry.modules.forEach(function (id) {
if (id !== 'ui/chrome') print(`require('${id}');\n`);
});
%>require('ui/chrome').bootstrap(/* xoxo */);

View file

@ -1,115 +1,7 @@
module.exports = function (kbnServer, server, config) {
if (!config.get('optimize.enabled')) return;
var _ = require('lodash');
var { resolve } = require('path');
var fromRoot = require('../utils/fromRoot');
var CachedOptimizer = require('./CachedOptimizer');
var WatchingOptimizer = require('./WatchingOptimizer');
var bundleDir = resolve(config.get('optimize.bundleDir'));
var status = kbnServer.status.create('optimize');
server.exposeStaticDir('/bundles/{path*}', bundleDir);
function logStats(tag, stats) {
if (config.get('logging.json')) {
server.log(['optimize', tag], _.pick(stats.toJson(), 'errors', 'warnings'));
} else {
server.log(['optimize', tag], `\n${ stats.toString({ colors: true }) }`);
}
}
function describeEntries(entries) {
let ids = _.pluck(entries, 'id').join('", "');
return `application${ entries.length === 1 ? '' : 's'} "${ids}"`;
}
function onMessage(handle, filter) {
filter = filter || _.constant(true);
process.on('message', function (msg) {
var optimizeMsg = msg && msg.optimizeMsg;
if (!optimizeMsg || !filter(optimizeMsg)) return;
handle(optimizeMsg);
});
}
var role = config.get('optimize._workerRole');
if (role === 'receive') {
// query for initial status
process.send(['WORKER_BROADCAST', { optimizeMsg: '?' }]);
onMessage(function (wrkrStatus) {
status[wrkrStatus.state](wrkrStatus.message);
});
}
if (role === 'send') {
let send = function () {
process.send(['WORKER_BROADCAST', { optimizeMsg: status }]);
};
status.on('change', send);
onMessage(send, _.partial(_.eq, '?'));
send();
}
let watching = config.get('optimize.watch');
let Optimizer = watching ? WatchingOptimizer : CachedOptimizer;
let optmzr = kbnServer.optimizer = new Optimizer({
sourceMaps: config.get('optimize.sourceMaps'),
bundleDir: bundleDir,
entries: _.map(kbnServer.uiExports.allApps(), function (app) {
return {
id: app.id,
deps: app.getRelatedPlugins(),
modules: app.getModules()
};
}),
plugins: kbnServer.plugins
});
server.on('close', _.bindKey(optmzr.disable || _.noop, optmzr));
kbnServer.mixin(require('./browserTests'))
.then(function () {
if (role === 'receive') return;
optmzr.on('bundle invalid', function () {
status.yellow('Source file change detected, reoptimizing source files');
});
optmzr.on('done', function (entries, stats) {
logStats('debug', stats);
status.green(`Optimization of ${describeEntries(entries)} complete`);
});
optmzr.on('error', function (entries, stats, err) {
if (stats) logStats('fatal', stats);
status.red('Optimization failure! ' + err.message);
});
return optmzr.init()
.then(function () {
let entries = optmzr.bundles.getMissingEntries();
if (!entries.length) {
if (watching) {
status.red('No optimizable applications found');
} else {
status.green('Reusing previously cached application source files');
}
return;
}
if (watching) {
status.yellow(`Optimizing and watching all application source files`);
} else {
status.yellow(`Optimizing and caching ${describeEntries(entries)}`);
}
optmzr.run();
return null;
});
});
let lazy = config.get('optimize.lazy');
let strategy = lazy ? require('./lazy/lazy') : require('./dist/dist');
return kbnServer.mixin(strategy);
};

View file

@ -0,0 +1,22 @@
let { Server } = require('hapi');
let { fromNode } = require('bluebird');
let Boom = require('boom');
module.exports = class LazyServer {
constructor(host, port, optimizer) {
this.optimizer = optimizer;
this.server = new Server({ minimal: true });
this.server.connection({
host: host,
port: port
});
}
async init() {
await this.optimizer.init();
this.optimizer.bindToServer(this.server);
await fromNode(cb => this.server.start(cb));
}
};

35
src/optimize/lazy/lazy.js Normal file
View file

@ -0,0 +1,35 @@
module.exports = async (kbnServer, server, config) => {
let { isWorker } = require('cluster');
if (!isWorker) {
throw new Error(`lazy optimization is only available in "watch" mode.`);
}
/**
* When running in lazy mode two workers/threads run in one
* of the modes: 'optmzr' or 'server'
*
* optmzr: this thread runs the LiveOptimizer and the LazyServer
* which serves the LiveOptimizer's output and blocks requests
* while the optimizer is running
*
* server: this thread runs the entire kibana server and proxies
* all requests for /bundles/* to the optmzr
*
* @param {string} process.env.kbnWorkerType
*/
switch (process.env.kbnWorkerType) {
case 'optmzr':
await kbnServer.mixin(require('./optmzrRole'));
break;
case 'server':
await kbnServer.mixin(require('./proxyRole'));
break;
default:
throw new Error(`unkown kbnWorkerType "${process.env.kbnWorkerType}"`);
}
};

View file

@ -0,0 +1,19 @@
module.exports = async (kbnServer, kibanaHapiServer, config) => {
let src = require('requirefrom')('src');
let fromRoot = src('utils/fromRoot');
let LazyServer = require('./LazyServer');
let LiveOptimizer = require('../LiveOptimizer');
let server = new LazyServer(
config.get('optimize.lazyHost'),
config.get('optimize.lazyPort'),
new LiveOptimizer({
env: kbnServer.bundles.env,
bundles: kbnServer.bundles,
sourceMaps: config.get('optimize.sourceMaps')
})
);
await server.init();
};

View file

@ -0,0 +1,16 @@
module.exports = (kbnServer, server, config) => {
server.route({
path: '/bundles/{path*}',
method: 'GET',
handler: {
proxy: {
host: config.get('optimize.lazyHost'),
port: config.get('optimize.lazyPort'),
passThrough: true,
xforward: true
}
}
});
};

View file

@ -16,7 +16,7 @@ require('ui/chrome')
'logo': 'url(' + kibanaLogoUrl + ') left no-repeat',
'smallLogo': 'url(' + kibanaLogoUrl + ') left no-repeat'
})
.setNavBackground('#222222')
.setNavBackground('black')
.setTabDefaults({
resetWhenActive: true,
trackLastPath: true,

View file

@ -22,14 +22,25 @@ module.exports = class KbnServer extends EventEmitter {
require('./config/setup'),
require('./http'),
require('./logging'),
require('./status'), // sets this.status
require('./plugins'), // sets this.plugins
require('./status'),
// find plugins and set this.plugins
require('./plugins/scan'),
// tell the config we are done loading plugins
require('./config/complete'),
require('../ui'), // sets this.uiExports
// setup this.uiExports and this.bundles
require('../ui'),
// ensure that all bundles are built, or that the
// lazy bundle server is running
require('../optimize'),
function () {
// finally, initialize the plugins
require('./plugins/initialize'),
() => {
if (this.config.get('server.autoListen')) {
this.listen();
}

View file

@ -75,8 +75,14 @@ module.exports = Joi.object({
bundleDir: Joi.string().default(fromRoot('optimize/bundles')),
viewCaching: Joi.boolean().default(Joi.ref('$prod')),
watch: Joi.boolean().default(Joi.ref('$dev')),
lazy: Joi.boolean().when('watch', {
is: true,
then: Joi.default(true),
otherwise: Joi.default(false)
}),
lazyPort: Joi.number().default(5602),
lazyHost: Joi.string().hostname().default('0.0.0.0'),
sourceMaps: Joi.boolean().default(Joi.ref('$dev')),
_workerRole: Joi.valid('send', 'receive', null).default(null)
}).default()
}).default();

View file

@ -1,6 +1,6 @@
let _ = require('lodash');
let Joi = require('joi');
let Promise = require('bluebird');
let { attempt, fromNode } = require('bluebird');
let { resolve } = require('path');
let { inherits } = require('util');
@ -33,63 +33,59 @@ module.exports = class Plugin {
};
}
init() {
let self = this;
let id = self.id;
let version = self.version;
let kbnStatus = self.kbnServer.status;
let server = self.kbnServer.server;
let config = self.kbnServer.config;
async init() {
let id = this.id;
let version = this.version;
let kbnStatus = this.kbnServer.status;
let server = this.kbnServer.server;
let config = this.kbnServer.config;
server.log(['plugins', 'debug'], {
tmpl: 'Initializing plugin <%= plugin.id %>',
plugin: self
plugin: this
});
self.status = kbnStatus.create(`plugin:${self.id}`);
return Promise.try(function () {
return self.getConfigSchema(Joi);
})
.then(function (schema) {
if (schema) config.extendSchema(id, schema);
else config.extendSchema(id, defaultConfigSchema);
})
.then(function () {
if (config.get([id, 'enabled'])) {
return self.externalCondition(config);
}
})
.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;
if (this.publicDir) {
server.exposeStaticDir(`/plugins/${id}/{path*}`, this.publicDir);
}
this.status = kbnStatus.create(`plugin:${this.id}`);
// setup plugin config
let schema = await this.getConfigSchema(Joi);
config.extendSchema(id, schema || defaultConfigSchema);
// ensure that the plugin is enabled
let enabled = config.get([id, 'enabled']) && this.externalCondition(config);
if (!enabled) {
// Only change the plugin status if it wasn't set by the externalCondition
if (this.status.state === 'uninitialized') {
this.status.disabled();
}
let register = function (server, options, next) {
server.expose('status', self.status);
Promise.try(self.externalInit, [server, options], self).nodeify(next);
};
return;
}
register.attributes = { name: id, version: version };
// setup the hapi register function and get on with it
let register = (server, options, next) => {
server.expose('status', this.status);
attempt(this.externalInit, [server, options], this).nodeify(next);
};
return Promise.fromNode(function (cb) {
server.register({
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');
}
});
register.attributes = { name: id, version: version };
await fromNode(cb => {
server.register({
register: register,
options: config.has(id) ? config.get(id) : null
}, cb);
});
// Only change the plugin status to green if the
// intial status has not been changed
if (this.status.state === 'uninitialized') {
this.status.green('Ready');
}
}
toJSON() {

View file

@ -10,18 +10,17 @@ module.exports = class Plugins extends Array {
}
new(path) {
var self = this;
var api = new PluginApi(this.kbnServer, path);
let output = [].concat(require(path)(api) || []);
[].concat(require(path)(api) || [])
.forEach(function (out) {
if (out instanceof api.Plugin) {
self._byId = null;
self.push(out);
for (let product of output) {
if (product instanceof api.Plugin) {
this._byId = null;
this.push(product);
} else {
throw new TypeError('unexpected plugin export ' + inspect(out));
throw new TypeError('unexpected plugin export ' + inspect(product));
}
});
}
}
get byId() {

View file

@ -1,25 +0,0 @@
module.exports = function (kbnServer, server, config) {
var _ = require('lodash');
var Promise = require('bluebird');
var Boom = require('boom');
var { join } = require('path');
server.exposeStaticDir('/plugins/{id}/{path*}', function (req) {
var id = req.params.id;
var plugin = kbnServer.plugins.byId[id];
return (plugin && plugin.publicDir) ? plugin.publicDir : Boom.notFound();
});
server.method('kbnPluginById', function (id, next) {
if (kbnServer.plugins.byId[id]) {
next(null, kbnServer.plugins.byId[id]);
} else {
next(Boom.notFound(`no plugin with the id "${id}"`));
}
});
return kbnServer.mixin(
require('./scan'),
require('./load')
);
};

View file

@ -1,11 +1,6 @@
module.exports = function (kbnServer, server, config) {
let _ = require('lodash');
let resolve = require('bluebird').resolve;
let all = require('bluebird').all;
let attempt = require('bluebird').attempt;
var Plugins = require('./Plugins');
var plugins = kbnServer.plugins = new Plugins(kbnServer);
let path = [];
let step = function (id, block) {
@ -23,26 +18,23 @@ module.exports = function (kbnServer, server, config) {
});
};
return resolve(kbnServer.pluginPaths)
.map(function (path) {
return plugins.new(path);
})
return resolve(kbnServer.plugins)
.then(function () {
if (!config.get('plugins.initialize')) {
server.log(['info'], 'Plugin initialization disabled.');
return [];
}
return _.toArray(plugins);
return _.toArray(kbnServer.plugins);
})
.each(function loadReqsAndInit(plugin) {
return step(plugin.id, function () {
return resolve(plugin.requiredIds).map(function (reqId) {
if (!plugins.byId[reqId]) {
if (!kbnServer.plugins.byId[reqId]) {
throw new Error(`Unmet requirement "${reqId}" for plugin "${plugin.id}"`);
}
return loadReqsAndInit(plugins.byId[reqId]);
return loadReqsAndInit(kbnServer.plugins.byId[reqId]);
})
.then(function () {
return plugin.init();

View file

@ -1,63 +1,62 @@
module.exports = function (kbnServer, server, config) {
module.exports = async (kbnServer, server, config) => {
let _ = require('lodash');
let Promise = require('bluebird');
let readdir = Promise.promisify(require('fs').readdir);
let stat = Promise.promisify(require('fs').stat);
let resolve = require('path').resolve;
let { fromNode } = require('bluebird');
let { readdir, stat } = require('fs');
let { resolve } = require('path');
let src = require('requirefrom')('src');
let { each } = src('utils/async');
var Plugins = require('./Plugins');
var plugins = kbnServer.plugins = new Plugins(kbnServer);
let scanDirs = [].concat(config.get('plugins.scanDirs') || []);
let absolutePaths = [].concat(config.get('plugins.paths') || []);
let pluginPaths = [].concat(config.get('plugins.paths') || []);
let debug = _.bindKey(server, 'log', ['plugins', 'debug']);
let warning = _.bindKey(server, 'log', ['plugins', 'warning']);
return Promise.map(scanDirs, function (dir) {
// scan all scanDirs to find pluginPaths
await each(scanDirs, async dir => {
debug({ tmpl: 'Scanning `<%= dir %>` for plugins', dir: dir });
return readdir(dir)
.catch(function (err) {
if (err.code !== 'ENOENT') {
throw err;
}
let filenames = null;
try {
filenames = await fromNode(cb => readdir(dir, cb));
} catch (err) {
if (err.code !== 'ENOENT') throw err;
filenames = [];
warning({
tmpl: '<%= err.code %>: Unable to scan non-existent directory for plugins "<%= dir %>"',
err: err,
dir: dir
});
return [];
})
.map(function (file) {
if (file === '.' || file === '..') return false;
let path = resolve(dir, file);
return stat(path).then(function (stat) {
return stat.isDirectory() ? path : false;
});
});
})
.then(function (dirs) {
return _([dirs, absolutePaths])
.flattenDeep()
.compact()
.uniq()
.value();
})
.filter(function (dir) {
let path;
try { path = require.resolve(dir); }
catch (e) { path = false; }
if (!path) {
warning({ tmpl: 'Skipping non-plugin directory at <%= dir %>', dir: dir });
return false;
} else {
require(path);
debug({ tmpl: 'Found plugin at <%= dir %>', dir: dir });
return true;
}
})
.then(function (pluginPaths) {
kbnServer.pluginPaths = pluginPaths;
await each(filenames, async name => {
if (name[0] === '.') return;
let path = resolve(dir, name);
let stats = await fromNode(cb => stat(path, cb));
if (stats.isDirectory()) {
pluginPaths.push(path);
}
});
});
for (let path of pluginPaths) {
let modulePath;
try {
modulePath = require.resolve(path);
} catch (e) {
warning({ tmpl: 'Skipping non-plugin directory at <%= path %>', path: path });
continue;
}
require(modulePath);
plugins.new(path);
debug({ tmpl: 'Found plugin at <%= modulePath %>', path: modulePath });
}
};

View file

@ -39,7 +39,7 @@ module.exports = function (kbnServer) {
});
server.decorate('reply', 'renderStatusPage', function () {
var app = _.get(kbnServer, 'uiExports.apps.hidden.byId.statusPage');
var app = _.get(kbnServer, 'apps.hidden.byId.statusPage');
var resp = app ? this.renderApp(app) : this(kbnServer.status.toString());
resp.code(kbnServer.status.isGreen() ? 200 : 503);
return resp;

View file

@ -19,7 +19,6 @@ class UiApp {
this.hidden = this.spec.hidden;
this.autoloadOverrides = this.spec.autoload;
this.templateName = this.spec.templateName || 'uiApp';
this.requireOptimizeGreen = this.spec.requireOptimizeGreen !== false;
this.getModules = _.once(this.getModules);
}
@ -34,18 +33,6 @@ class UiApp {
.value();
}
getRelatedPlugins() {
var pluginsById = this.uiExports.kbnServer.plugins.byId;
return _.transform(this.getModules(), function (plugins, id) {
var matches = id.match(/^plugins\/([^\/]+)(?:\/|$)/);
if (!matches) return;
var plugin = pluginsById[matches[1]];
if (_.includes(plugins, plugin)) return;
plugins.push(plugin);
}, []);
}
toJSON() {
return _.pick(this, ['id', 'title', 'description', 'icon', 'main']);
}

69
src/ui/UiBundle.js Normal file
View file

@ -0,0 +1,69 @@
let { join } = require('path');
let { promisify } = require('bluebird');
let read = promisify(require('fs').readFile);
let write = promisify(require('fs').writeFile);
let unlink = promisify(require('fs').unlink);
let stat = promisify(require('fs').stat);
module.exports = class UiBundle {
constructor(opts) {
opts = opts || {};
this.env = opts.env;
this.id = opts.id;
this.plugins = opts.plugins;
this.modules = opts.modules;
this.template = opts.template;
let pathBase = join(this.env.workingDir, this.id);
this.entryPath = `${pathBase}.entry.js`;
this.outputPath = `${pathBase}.bundle.js`;
}
renderContent() {
return this.template({
env: this.env,
bundle: this
});
}
async readEntryFile() {
try {
let content = await read(this.entryPath);
return content.toString('utf8');
}
catch (e) {
return null;
}
}
async writeEntryFile() {
return await write(this.entryPath, this.renderContent(), { encoding: 'utf8' });
}
async clearBundleFile() {
try { await unlink(this.outputPath); }
catch (e) { return null; }
}
async checkForExistingOutput() {
try {
await stat(this.outputPath);
return true;
}
catch (e) {
return false;
}
}
toJSON() {
return {
id: this.id,
plugins: this.plugins,
modules: this.modules,
entryPath: this.entryPath,
outputPath: this.outputPath
};
}
};

View file

@ -0,0 +1,68 @@
let { pull, transform, pluck } = require('lodash');
let { join } = require('path');
let { resolve, promisify } = require('bluebird');
let rimraf = promisify(require('rimraf'));
let mkdirp = promisify(require('mkdirp'));
let unlink = promisify(require('fs').unlink);
let readdir = promisify(require('fs').readdir);
let readSync = require('fs').readFileSync;
let UiBundle = require('./UiBundle');
let appEntryTemplate = require('./appEntryTemplate');
class UiBundleCollection {
constructor(bundlerEnv) {
this.each = [];
this.env = bundlerEnv;
}
addApp(app) {
this.each.push(new UiBundle({
id: app.id,
modules: app.getModules(),
template: appEntryTemplate,
env: this.env
}));
}
async ensureDir() {
await mkdirp(this.env.workingDir);
}
async writeEntryFiles() {
await this.ensureDir();
for (let bundle of this.each) {
let existing = await bundle.readEntryFile();
let expected = bundle.renderContent();
if (existing !== expected) {
await bundle.writeEntryFile();
await bundle.clearBundleFile();
}
}
}
async filterCachedBundles() {
for (let bundle of this.each.slice()) {
let exists = await bundle.checkForExistingOutput();
if (exists) pull(this.each, bundle);
}
}
toWebpackEntries() {
return transform(this.each, function (entries, bundle) {
entries[bundle.id] = bundle.entryPath;
}, {});
}
getIds() {
return pluck(this.each, 'id');
}
toJSON() {
return this.each;
}
}
module.exports = UiBundleCollection;

147
src/ui/UiBundlerEnv.js Normal file
View file

@ -0,0 +1,147 @@
let { includes, flow, escapeRegExp } = require('lodash');
let { isString, isArray, isPlainObject, get } = require('lodash');
let { keys } = require('lodash');
let fromRoot = require('../utils/fromRoot');
let asRegExp = flow(
escapeRegExp,
function (path) {
return path + '(?:\\.js)?$';
},
RegExp
);
let arr = v => [].concat(v || []);
module.exports = class UiBundlerEnv {
constructor(workingDir) {
// the location that bundle entry files and all compiles files will
// be written
this.workingDir = workingDir;
// the context that the bundler is running in, this is not officially
// used for anything but it is serialized into the entry file to ensure
// that they are invalidated when the context changes
this.context = {};
// the plugins that are used to build this environment
// are tracked and embedded into the entry file so that when the
// environment changes we can rebuild the bundles
this.pluginInfo = [];
// regular expressions which will prevent webpack from parsing the file
this.noParse = [];
// webpack aliases, like require paths, mapping a prefix to a directory
this.aliases = {
ui: fromRoot('src/ui/public'),
testHarness: fromRoot('src/testHarness/public')
};
// map of which plugins created which aliases
this.aliasOwners = {};
// webpack loaders map loader configuration to regexps
this.loaders = [];
}
consumePlugin(plugin) {
let tag = `${plugin.id}@${plugin.version}`;
if (includes(this.pluginInfo, tag)) return;
if (plugin.publicDir) {
this.aliases[`plugins/${plugin.id}`] = plugin.publicDir;
}
this.pluginInfo.push(tag);
}
exportConsumer(type) {
switch (type) {
case 'loaders':
return (plugin, spec) => {
for (let loader of arr(spec)) this.addLoader(loader);
};
case 'noParse':
return (plugin, spec) => {
for (let re of arr(spec)) this.addNoParse(re);
};
case 'modules':
return (plugin, spec) => {
for (let id of keys(spec)) this.addModule(id, spec[id], plugin.id);
};
}
}
addContext(key, val) {
this.context[key] = val;
}
addLoader(loader) {
this.loaders.push(loader);
}
addNoParse(regExp) {
this.noParse.push(regExp);
}
addModule(id, spec, pluginId) {
this.claim(id, pluginId);
// configurable via spec
let path;
let parse = true;
let imports = null;
let exports = null;
let expose = null;
// basic style, just a path
if (isString(spec)) path = spec;
if (isArray(spec)) {
path = spec[0];
imports = spec[1];
exports = spec[2];
}
if (isPlainObject(spec)) {
path = spec.path;
parse = get(spec, 'parse', parse);
imports = get(spec, 'imports', imports);
exports = get(spec, 'exports', exports);
expose = get(spec, 'expose', expose);
}
if (!path) {
throw new TypeError('Invalid spec definition, unable to identify path');
}
this.aliases[id] = path;
let loader = [];
if (imports) {
loader.push(`imports?${imports}`);
}
if (exports) loader.push(`exports?${exports}`);
if (expose) loader.push(`expose?${expose}`);
if (loader.length) this.loaders.push({ test: asRegExp(path), loader: loader.join('!') });
if (!parse) this.noParse.push(asRegExp(path));
}
claim(id, pluginId) {
let owner = pluginId ? `Plugin ${pluginId}` : 'Kibana Server';
// TODO(spalger): we could do a lot more to detect colliding module defs
var existingOwner = this.aliasOwners[id] || this.aliasOwners[`${id}$`];
if (existingOwner) {
throw new TypeError(`${owner} attempted to override export "${id}" from ${existingOwner}`);
}
this.aliasOwners[id] = owner;
}
};

View file

@ -5,57 +5,59 @@ var UiApps = require('./UiApps');
class UiExports {
constructor(kbnServer) {
this.kbnServer = kbnServer;
this.apps = new UiApps(this);
this.aliases = {};
this.exportConsumer = _.memoize(this.exportConsumer);
kbnServer.plugins.forEach(_.bindKey(this, 'consumePlugin'));
this.consumers = [];
}
consumePlugin(plugin) {
var self = this;
var types = _.keys(plugin.uiExportsSpecs);
if (!types) return false;
var unkown = _.reject(types, self.exportConsumer, self);
var unkown = _.reject(types, this.exportConsumer, this);
if (unkown.length) {
throw new Error('unknown export types ' + unkown.join(', ') + ' in plugin ' + plugin.id);
}
types.forEach(function (type) {
self.exportConsumer(type)(plugin, plugin.uiExportsSpecs[type]);
for (let consumer of this.consumers) {
consumer.consumePlugin && consumer.consumePlugin(plugin);
}
types.forEach((type) => {
this.exportConsumer(type)(plugin, plugin.uiExportsSpecs[type]);
});
}
addConsumer(consumer) {
this.consumers.push(consumer);
}
exportConsumer(type) {
var self = this;
for (let consumer of this.consumers) {
if (!consumer.exportConsumer) continue;
let fn = consumer.exportConsumer(type);
if (fn) return fn;
}
switch (type) {
case 'app':
return function (plugin, spec) {
return (plugin, spec) => {
spec = _.defaults({}, spec, { id: plugin.id });
plugin.app = self.apps.new(spec);
plugin.app = this.apps.new(spec);
};
case 'visTypes':
case 'fieldFormats':
case 'spyModes':
return function (plugin, spec) {
self.aliases[type] = _.union(self.aliases[type] || [], spec);
};
case 'modules':
case 'loaders':
case 'noParse':
return function (plugin, spec) {
plugin.uiExportsSpecs[type] = spec;
return (plugin, spec) => {
this.aliases[type] = _.union(this.aliases[type] || [], spec);
};
case 'aliases':
return function (plugin, specs) {
_.forOwn(specs, function (spec, adhocType) {
self.aliases[adhocType] = _.union(self.aliases[adhocType] || [], spec);
return (plugin, specs) => {
_.forOwn(specs, (spec, adhocType) => {
this.aliases[adhocType] = _.union(this.aliases[adhocType] || [], spec);
});
};
}
@ -81,9 +83,17 @@ class UiExports {
.value();
}
allApps() {
getAllApps() {
return _.union(this.apps, this.apps.hidden);
}
getApp(id) {
return this.apps.byId[id];
}
getHiddenApp(id) {
return this.apps.hidden.byId[id];
}
}
module.exports = UiExports;

View file

@ -0,0 +1,33 @@
module.exports = require('lodash').template(
`
/**
* Optimized application entry file
*
* This is programatically created and updated, do not modify
*
* context: <%= JSON.stringify(env.context) %>
<%
env.pluginInfo.sort().forEach(function (plugin) {
print(\` * - \${plugin}\n\`);
});
%> *
*/
require('ui/chrome');
<%
bundle.modules.forEach(function (id) {
if (id !== 'ui/chrome') {
print(\`require('\${id}');\n\`);
}
});
%>
require('ui/chrome').bootstrap(/* xoxo */);
`
);

View file

@ -3,11 +3,29 @@ module.exports = function (kbnServer, server, config) {
let Boom = require('boom');
let formatUrl = require('url').format;
let { join, resolve } = require('path');
let UiExports = require('./UiExports');
let UiBundleCollection = require('./UiBundleCollection');
let UiBundlerEnv = require('./UiBundlerEnv');
let uiExports = kbnServer.uiExports = new UiExports(kbnServer);
let apps = uiExports.apps;
let hiddenApps = uiExports.apps.hidden;
let bundlerEnv = new UiBundlerEnv(config.get('optimize.bundleDir'));
bundlerEnv.addContext('env', config.get('env.name'));
bundlerEnv.addContext('sourceMaps', config.get('optimize.sourceMaps'));
bundlerEnv.addContext('kbnVersion', config.get('pkg.version'));
bundlerEnv.addContext('buildNum', config.get('pkg.buildNum'));
uiExports.addConsumer(bundlerEnv);
for (let plugin of kbnServer.plugins) {
uiExports.consumePlugin(plugin);
}
let bundles = kbnServer.bundles = new UiBundleCollection(bundlerEnv);
for (let app of uiExports.getAllApps()) {
bundles.addApp(app);
}
// render all views from the ui/views directory
server.setupViews(resolve(__dirname, 'views'));
@ -17,7 +35,7 @@ module.exports = function (kbnServer, server, config) {
path: '/apps',
method: 'GET',
handler: function (req, reply) {
let switcher = hiddenApps.byId.switcher;
let switcher = uiExports.getHiddenApp('switcher');
if (!switcher) return reply(Boom.notFound('app switcher not installed'));
return reply.renderApp(switcher);
}
@ -28,7 +46,7 @@ module.exports = function (kbnServer, server, config) {
path: '/api/apps',
method: 'GET',
handler: function (req, reply) {
return reply(apps);
return reply(uiExports.apps);
}
});
@ -37,7 +55,7 @@ module.exports = function (kbnServer, server, config) {
method: 'GET',
handler: function (req, reply) {
let id = req.params.id;
let app = apps.byId[id];
let app = uiExports.apps.byId[id];
if (!app) return reply(Boom.notFound('Unkown app ' + id));
if (kbnServer.status.isGreen()) {
@ -49,27 +67,9 @@ module.exports = function (kbnServer, server, config) {
});
server.decorate('reply', 'renderApp', function (app) {
if (app.requireOptimizeGreen) {
let optimizeStatus = kbnServer.status.get('optimize');
switch (optimizeStatus && optimizeStatus.state) {
case 'yellow':
return this(`
<html>
<head><meta http-equiv="refresh" content="1"></head>
<body>${optimizeStatus.message}</body>
</html>
`).code(503);
case 'red':
return this(`
<html><body>${optimizeStatus.message}</body></html>
`).code(500);
}
}
let payload = {
app: app,
appCount: apps.length,
appCount: uiExports.apps.length,
version: kbnServer.version,
buildSha: _.get(kbnServer, 'build.sha', '@@buildSha'),
buildNumber: _.get(kbnServer, 'build.number', '@@buildNum'),

9
src/utils/async.js Normal file
View file

@ -0,0 +1,9 @@
let { all } = require('bluebird');
exports.each = async (arr, fn) => {
await all(arr.map(fn)).return(undefined);
};
exports.map = async (arr, fn) => {
await all(arr.map(fn));
};