Ported kitematic setup

This commit is contained in:
Jeffrey Morgan 2015-01-12 09:28:29 -05:00
parent be51f26065
commit 13cd6ed0b4
22 changed files with 789 additions and 134 deletions

15
app/Container.react.js Normal file
View File

@ -0,0 +1,15 @@
var React = require('react');
var Router = require('react-router');
var Route = Router.Route;
var NotFoundRoute = Router.NotFoundRoute;
var DefaultRoute = Router.DefaultRoute;
var Link = Router.Link;
var RouteHandler = Router.RouteHandler;
var Container = React.createClass({
render: function () {
return <p>Hello</p>;
}
});
module.exports = Container;

69
app/Containers.react.js Normal file
View File

@ -0,0 +1,69 @@
var React = require('react');
var Router = require('react-router');
var Route = Router.Route;
var NotFoundRoute = Router.NotFoundRoute;
var DefaultRoute = Router.DefaultRoute;
var Link = Router.Link;
var RouteHandler = Router.RouteHandler;
var Navigation= Router.Navigation;
var async = require('async');
var docker = require('./docker.js');
var ContainerList = React.createClass({
render: function () {
var containers = this.props.containers.map(function (container) {
return <li key={container.Id}><Link to="container" params={container}>{container.Name.replace('/', '')}</Link></li>
});
return (
<ul>
{containers}
</ul>
);
}
});
var Containers = React.createClass({
mixins: [Navigation],
getInitialState: function() {
return {containers: []};
},
update: function () {
var self = this;
docker.client().listContainers({all: true}, function (err, containers) {
async.map(containers, function(container, callback) {
docker.client().getContainer(container.Id).inspect(function (err, data) {
callback(null, data);
});
}, function (err, results) {
if (results.length > 0) {
self.transitionTo('container', {Id: results[0].Id})
}
self.setState({containers: results});
});
});
},
componentDidMount: function () {
this.update();
var self = this;
docker.client().getEvents(function (err, stream) {
if (err) {
throw err;
}
stream.setEncoding('utf8');
stream.on('data', function (data) {
self.update();
});
});
},
render: function () {
return (
<div>
<ContainerList containers={this.state.containers}/>
<RouteHandler/>
</div>
);
}
});
module.exports = Containers;

37
app/Radial.react.js Normal file
View File

@ -0,0 +1,37 @@
var React = require('react/addons');
var Radial = React.createClass({
render: function () {
var percentage;
if (this.props.progress && !this.props.spin) {
percentage = (
<div className="percentage"></div>
);
} else {
percentage = <div></div>;
}
var classes = React.addons.classSet({
'radial-progress': true,
'radial-spinner': this.props.spin
});
return (
<div className={classes} data-progress={this.props.progress}>
<div className="circle">
<div className="mask full">
<div className="fill"></div>
</div>
<div className="mask half">
<div className="fill"></div>
<div className="fill fix"></div>
</div>
<div className="shadow"></div>
</div>
<div className="inset">
{percentage}
</div>
</div>
);
}
});
module.exports = Radial;

195
app/Setup.react.js Normal file
View File

@ -0,0 +1,195 @@
var React = require('react');
var Router = require('react-router');
var Radial = require('./Radial.react.js');
var async = require('async');
var assign = require('object-assign');
var fs = require('fs');
var path = require('path');
var boot2docker = require('./boot2docker.js');
var virtualbox = require('./virtualbox.js');
var util = require('./util.js');
var docker = require('./docker.js');
var setupSteps = [
{
run: function (callback, progressCallback) {
var installed = virtualbox.installed();
if (!installed) {
util.download('https://s3.amazonaws.com/kite-installer/' + virtualbox.INSTALLER_FILENAME, path.join(process.cwd(), 'resources', virtualbox.INSTALLER_FILENAME), virtualbox.INSTALLER_CHECKSUM, function (err) {
if (err) {callback(err); return;}
virtualbox.install(function (err) {
if (!virtualbox.installed()) {
callback('VirtualBox could not be installed. The installation either failed or was cancelled. Please try closing all VirtualBox instances and try again.');
} else {
callback(err);
}
});
}, function (progress) {
progressCallback(progress);
});
} else {
virtualbox.version(function (err, installedVersion) {
if (err) {callback(err); return;}
if (util.compareVersions(installedVersion, virtualbox.REQUIRED_VERSION) < 0) {
// Download a newer version of Virtualbox
util.downloadFile(Setup.BASE_URL + virtualbox.INSTALLER_FILENAME, path.join(util.getResourceDir(), virtualbox.INSTALLER_FILENAME), virtualbox.INSTALLER_CHECKSUM, function (err) {
if (err) {callback(err); return;}
virtualbox.kill(function (err) {
if (err) {callback(err); return;}
virtualbox.install(function (err) {
if (err) {callback(err); return;}
virtualbox.version(function (err, installedVersion) {
if (err) {callback(err); return;}
if (util.compareVersions(installedVersion, virtualbox.REQUIRED_VERSION) < 0) {
callback('VirtualBox could not be installed. The installation either failed or was cancelled. Please try closing all VirtualBox instances and try again.');
} else {
callback(err);
}
});
});
});
}, function (progress) {
progressCallback(progress);
});
} else {
callback();
}
});
}
},
message: 'Downloading VirtualBox...'
},
{
run: function (callback) {
virtualbox.deleteVM('kitematic-vm', function (err, removed) {
if (err) {
console.log(err);
}
callback();
});
},
message: 'Cleaning up existing Docker VM...'
},
// Initialize Boot2Docker if necessary.
{
run: function (callback) {
boot2docker.exists(function (err, exists) {
if (err) { callback(err); return; }
if (!exists) {
boot2docker.init(function (err) {
callback(err);
});
} else {
if (!boot2docker.sshKeyExists()) {
callback('Boot2Docker SSH key doesn\'t exist. Fix by removing the existing Boot2Docker VM and re-run the installer. This usually occurs because an old version of Boot2Docker is installed.');
} else {
boot2docker.isoVersion(function (err, version) {
if (err || util.compareVersions(version, boot2docker.version()) < 0) {
boot2docker.stop(function(err) {
boot2docker.upgrade(function (err) {
callback(err);
});
});
} else {
callback();
}
});
}
}
});
},
message: 'Setting up the Docker VM...'
},
{
run: function (callback) {
boot2docker.waitWhileStatus('saving', function (err) {
boot2docker.status(function (err, status) {
if (err) {callback(err); return;}
if (status !== 'running') {
boot2docker.start(function (err) {
callback(err);
});
} else {
callback();
}
});
});
},
message: 'Starting the Docker VM...'
},
{
run: function (callback) {
boot2docker.ip(function (err, ip) {
if (err) {callback(err); return;}
console.log('Setting host IP to: ' + ip);
// Docker.setHost(ip);
callback(err);
});
},
message: 'Detecting Docker VM...'
}
];
var Setup = React.createClass({
mixins: [ Router.Navigation ],
render: function () {
var radial;
if (this.state.progress) {
radial = <Radial progress={this.state.progress}/>;
} else {
radial = <Radial spin="true" progress="92"/>;
}
return (
<div className="setup">
{radial}
<p>{this.state.message}</p>
</div>
);
},
componentWillMount: function () {
this.setState({});
},
componentDidMount: function () {
var self = this;
this.setup(function (err) {
boot2docker.ip(function (err, ip) {
docker.setHost(ip);
self.transitionTo('containers');
});
});
},
setup: function (callback) {
var self = this;
var currentStep = 0;
async.eachSeries(setupSteps, function (step, callback) {
console.log('Performing step ' + currentStep);
self.setState({progress: 0});
self.setState({message: step.message});
step.run(function (err) {
if (err) {
callback(err);
} else {
currentStep += 1;
callback();
}
}, function (progress) {
self.setState({progress: progress});
});
}, function (err) {
if (err) {
// if any of the steps fail
console.log('Kitematic setup failed at step ' + currentStep);
console.log(err);
self.setState({error: err});
callback(err);
} else {
// Setup Finished
console.log('Setup finished.');
callback();
}
});
}
});
module.exports = Setup;

View File

@ -1,18 +0,0 @@
var flux = require('flux-react');
var actions = require('./actions.js');
module.exports = flux.createStore({
messages: [],
actions: [
actions.addMessage
],
addMessage: function (message) {
this.messages.push(message);
this.emitChange();
},
exports: {
getMessages: function () {
return this.messages;
}
}
});

View File

@ -1,5 +0,0 @@
var flux = require('flux-react');
module.exports = flux.createActions([
'addMessage'
]);

View File

@ -6,7 +6,6 @@ var async = require('async');
var cmdExec = function (cmd, callback) { var cmdExec = function (cmd, callback) {
exec(cmd, function (stderr, stdout, code) { exec(cmd, function (stderr, stdout, code) {
console.log(stderr, stdout, code);
if (code) { if (code) {
callback('Exit code ' + code + ': ' + stderr); callback('Exit code ' + code + ': ' + stderr);
} else { } else {
@ -21,12 +20,10 @@ var homeDir = function () {
var Boot2Docker = { var Boot2Docker = {
version: function () { version: function () {
var packagejson = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf8')); return JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf8'))['boot2docker-version'];
return packagejson['boot2docker-version'];
}, },
cliVersion: function (callback) { cliVersion: function (callback) {
cmdExec([Boot2Docker.command(), 'version'], function (err, out) { cmdExec([Boot2Docker.command(), 'version'], function (err, out) {
console.log(err, out);
if (err) { if (err) {
callback(err); callback(err);
return; return;
@ -61,7 +58,6 @@ var Boot2Docker = {
}, },
status: function (callback) { status: function (callback) {
cmdExec([Boot2Docker.command(), 'status'], function (err, out) { cmdExec([Boot2Docker.command(), 'status'], function (err, out) {
console.log(err, out);
if (err) { if (err) {
callback(err); callback(err);
return; return;
@ -192,13 +188,10 @@ var Boot2Docker = {
}, },
sshKeyExists: function () { sshKeyExists: function () {
return fs.existsSync(path.join(homeDir(), '.ssh', 'id_boot2docker')); return fs.existsSync(path.join(homeDir(), '.ssh', 'id_boot2docker'));
} },
};
module.exports = Boot2Docker; // Todo: move me to setup
waitWhileStatus: function (status, callback) {
//TODO: move me to setup
Boot2Docker.waitWhileStatus = function (status, callback) {
var current = status; var current = status;
async.whilst(function () { async.whilst(function () {
return current === status; return current === status;
@ -214,4 +207,7 @@ Boot2Docker.waitWhileStatus = function (status, callback) {
}, function (err) { }, function (err) {
callback(err); callback(err);
}); });
}
}; };
module.exports = Boot2Docker;

26
app/docker.js Normal file
View File

@ -0,0 +1,26 @@
var fs = require('fs');
var path = require('path');
var dockerode = require('dockerode');
var Docker = {
host: null,
setHost: function(host) {
this.host = host;
},
client: function () {
var certDir = path.join(process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME'], '.boot2docker/certs/boot2docker-vm');
if (!fs.existsSync(certDir)) {
return null;
}
return new dockerode({
protocol: 'https',
host: this.host,
port: 2376,
ca: fs.readFileSync(path.join(certDir, 'ca.pem')),
cert: fs.readFileSync(path.join(certDir, 'cert.pem')),
key: fs.readFileSync(path.join(certDir, 'key.pem'))
});
}
};
module.exports = Docker;

View File

@ -4,14 +4,8 @@
<link rel="stylesheet" href="main.css"/> <link rel="stylesheet" href="main.css"/>
</head> </head>
<body> <body>
<script src="main.js"></script>
<script src="https://cdn.ravenjs.com/1.1.15/jquery,native/raven.min.js"></script> <script src="https://cdn.ravenjs.com/1.1.15/jquery,native/raven.min.js"></script>
<script src="http://localhost:35729/livereload.js"></script> <script src="http://localhost:35729/livereload.js"></script>
<script>
Raven.config('https://0a5f032d745d4acaae94ce46f762c586@app.getsentry.com/35057', {
// we highly recommend restricting exceptions to a domain in order to filter out clutter
// whitelistUrls: ['example.com/scripts/']
}).install();
</script>
</body> </body>
</html> </html>

View File

@ -1,12 +1,29 @@
var React = require('react'); var React = require('react');
var Router = require('react-router'); var Router = require('react-router');
var RetinaImage = require('react-retina-image');
var Route = Router.Route; var Route = Router.Route;
var NotFoundRoute = Router.NotFoundRoute; var NotFoundRoute = Router.NotFoundRoute;
var DefaultRoute = Router.DefaultRoute; var DefaultRoute = Router.DefaultRoute;
var Link = Router.Link; var Link = Router.Link;
var RouteHandler = Router.RouteHandler; var RouteHandler = Router.RouteHandler;
var async = require('async');
var docker = require('./docker.js');
var boot2docker = require('./boot2docker.js'); var boot2docker = require('./boot2docker.js');
var Setup = require('./Setup.react');
var Containers = require('./Containers.react');
var Container = require('./Container.react');
var Radial = require('./Radial.react');
var NoContainers = React.createClass({
render: function () {
return (
<div>
<Radial spin="true" progress="92"/>
</div>
);
}
});
var App = React.createClass({ var App = React.createClass({
render: function () { render: function () {
@ -16,67 +33,27 @@ var App = React.createClass({
} }
}); });
var Setup = React.createClass({
render: function () {
return (
<p>Hello!</p>
);
},
componentWillMount: function () {
},
setup: function () {
},
steps: [
]
});
var Containers = React.createClass({
render: function () {
return (
<div>
<ContainerList/>
<RouteHandler/>
</div>
);
}
});
var ContainerList = React.createClass({
render: function () {
return (
<ul>
<li>Container 1</li>
<li>Container 2</li>
<li>Container 3</li>
<li>Container 4</li>
<li>Container 5</li>
</ul>
);
}
});
var NoContainers = React.createClass({
render: function () {
return (
<p>No containers</p>
);
}
});
var routes = ( var routes = (
<Route name="app" path="/" handler={App}> <Route name="app" path="/" handler={App}>
<DefaultRoute handler={Setup}/>
<Route name="containers" handler={Containers}> <Route name="containers" handler={Containers}>
<Route name="container" path=":Id" handler={Container}>
</Route>
<DefaultRoute handler={NoContainers}/> <DefaultRoute handler={NoContainers}/>
</Route> </Route>
<DefaultRoute handler={Setup}/>
<Route name="setup" handler={Setup}> <Route name="setup" handler={Setup}>
</Route> </Route>
</Route> </Route>
); );
Router.run(routes, function (Handler) { Router.run(routes, function (Handler) {
boot2docker.ip(function (err, ip) {
docker.setHost(ip);
React.render(<Handler/>, document.body); React.render(<Handler/>, document.body);
});
}); });
if (process.env.NODE_ENV !== 'development') {
Raven.config('https://0a5f032d745d4acaae94ce46f762c586@app.getsentry.com/35057', {
}).install();
}

View File

@ -1,4 +1,6 @@
@import "bootstrap/bootstrap.less"; @import "bootstrap/bootstrap.less";
@import "setup.less";
@import "radial.less";
body { body {
background: white; background: white;

111
app/styles/radial.less Normal file
View File

@ -0,0 +1,111 @@
@import "variables.less";
@-webkit-keyframes rotating {
from{
-webkit-transform: rotate(0deg);
}
to{
-webkit-transform: rotate(360deg);
}
}
.radial-progress {
&.radial-spinner {
-webkit-animation: rotating 1.2s linear infinite;
}
@circle-size: 96px;
@circle-background: transparent;
@inset-size: 92px;
@inset-color: white;
@transition-length: 1s;
// @percentage-color: #3FD899;
@percentage-font-size: 14px;
@percentage-text-width: 57px;
margin: 0 auto;
width: @circle-size;
height: @circle-size;
background-color: @circle-background;
border-radius: 100%;
.circle {
.mask, .fill, .shadow {
width: @circle-size;
height: @circle-size;
position: absolute;
border-radius: 100%;
}
.mask, .fill {
-webkit-backface-visibility: hidden;
transition: -webkit-transform @transition-length;
transition: -ms-transform @transition-length;
transition: transform @transition-length;
border-radius: 100%;
}
.mask {
clip: rect(0px, @circle-size, @circle-size, @circle-size/2.0);
.fill {
clip: rect(0px, @circle-size/2.0, @circle-size, 0px);
background-color: @brand-action;
}
}
}
.inset {
width: @inset-size;
height: @inset-size;
position: absolute;
margin-left: (@circle-size - @inset-size) / 2.0;
margin-top: (@circle-size - @inset-size) / 2.0;
background-color: @inset-color;
border-radius: 100%;
.percentage {
width: @percentage-text-width;
position: absolute;
top: (@inset-size - @percentage-font-size) / 2.0;
left: (@inset-size - @percentage-text-width) / 2.0;
line-height: 1;
text-align: center;
// color: @percentage-color;
font-weight: 500;
font-size: @percentage-font-size;
}
}
&.radial-negative .circle .mask .fill {
background-color: @brand-negative;
}
&.radial-positive .circle .mask .fill {
background-color: @brand-positive;
}
@i: 0;
@increment: 180deg / 100;
.loop (@i) when (@i <= 100) {
&[data-progress="@{i}"] {
.circle {
.mask.full, .fill {
-webkit-transform: rotate(@increment * @i);
-ms-transform: rotate(@increment * @i);
transform: rotate(@increment * @i);
}
.fill.fix {
-webkit-transform: rotate(@increment * @i * 2);
-ms-transform: rotate(@increment * @i * 2);
transform: rotate(@increment * @i * 2);
}
}
.inset .percentage:before {
content: "@{i}%"
}
}
.loop(@i + 1);
}
.loop(@i);
}

8
app/styles/setup.less Normal file
View File

@ -0,0 +1,8 @@
.setup {
margin-top: 25%;
text-align: center;
p {
margin-top: 20px;
}
}

View File

@ -0,0 +1,3 @@
@brand-action: #4A9AEC;
@brand-positive: #3AD86D;
@brand-negative: #F74B1F;

90
app/util.js Normal file
View File

@ -0,0 +1,90 @@
var path = require('path');
var fs = require('fs');
var nodeCrypto = require('crypto');
var request = require('request');
var progress = require('request-progress');
var ncp = require('ncp').ncp;
var exec = require('exec');
var Util = {
download: function (url, filename, checksum, callback, progressCallback) {
var doDownload = function () {
progress(request(url), {
throttle: 250
}).on('progress', function (state) {
progressCallback(state.percent);
}).on('error', function (err) {
callback(err);
}).pipe(fs.createWriteStream(filename)).on('error', function (err) {
callback(err);
}).on('close', function (err) {
callback(err);
});
};
// Compare checksum to see if it already exists first
if (fs.existsSync(filename)) {
var existingChecksum = nodeCrypto.createHash('sha256').update(fs.readFileSync(filename), 'utf8').digest('hex');
console.log(existingChecksum);
if (existingChecksum !== checksum) {
fs.unlinkSync(filename);
doDownload();
} else {
callback();
}
} else {
doDownload();
}
},
compareVersions: function (v1, v2, options) {
var lexicographical = options && options.lexicographical,
zeroExtend = options && options.zeroExtend,
v1parts = v1.split('.'),
v2parts = v2.split('.');
function isValidPart(x) {
return (lexicographical ? /^\d+[A-Za-z]*$/ : /^\d+$/).test(x);
}
if (!v1parts.every(isValidPart) || !v2parts.every(isValidPart)) {
return NaN;
}
if (zeroExtend) {
while (v1parts.length < v2parts.length) {
v1parts.push('0');
}
while (v2parts.length < v1parts.length) {
v2parts.push('0');
}
}
if (!lexicographical) {
v1parts = v1parts.map(Number);
v2parts = v2parts.map(Number);
}
for (var i = 0; i < v1parts.length; ++i) {
if (v2parts.length === i) {
return 1;
}
if (v1parts[i] === v2parts[i]) {
continue;
}
else if (v1parts[i] > v2parts[i]) {
return 1;
}
else {
return -1;
}
}
if (v1parts.length !== v2parts.length) {
return -1;
}
return 0;
}
};
module.exports = Util;

109
app/virtualbox.js Normal file
View File

@ -0,0 +1,109 @@
var fs = require('fs');
var exec = require('exec');
var path = require('path');
var async = require('async');
var VirtualBox = {
REQUIRED_VERSION: '4.3.18',
INCLUDED_VERSION: '4.3.18',
INSTALLER_FILENAME: 'virtualbox-4.3.18.pkg',
INSTALLER_CHECKSUM: '5836c94481c460c648b9216386591a2915293ac86b9bb6c57746637796af6af2',
command: function () {
return '/usr/bin/VBoxManage';
},
installed: function () {
return fs.existsSync('/usr/bin/VBoxManage') && fs.existsSync('/Applications/VirtualBox.app/Contents/MacOS/VirtualBox');
},
install: function (callback) {
// -W waits for the process to close before finishing.
exec('open -W ' + path.join(process.cwd(), 'resources', this.INSTALLER_FILENAME).replace(' ', '\\ '), function (stderr, stdout, code) {
if (code) {
callback(stderr);
return;
}
callback(null);
});
},
version: function (callback) {
if (!this.installed()) {
callback('VirtualBox not installed.');
return;
}
exec('/usr/bin/VBoxManage -v', function (stderr, stdout, code) {
if (code) {
callback(stderr);
return;
}
// Output is x.x.xryyyyyy
var match = stdout.match(/(\d+\.\d+\.\d+).*/);
if (!match || match.length < 2) {
callback('VBoxManage -v output format not recognized.');
return;
}
callback(null, match[1]);
});
},
saveVMs: function (callback) {
if (!this.installed()) {
callback('VirtualBox not installed.');
return;
}
exec('list runningvms | sed -E \'s/.*\\{(.*)\\}/\\1/\' | xargs -L1 -I {} VBoxManage controlvm {} savestate', function (stderr, stdout, code) {
if (code) {
callback(stderr);
} else {
callback();
}
});
},
kill: function (callback) {
this.saveRunningVMs(function (err) {
if (err) {callback(err); return;}
exec('pkill VirtualBox', function (stderr, stdout, code) {
if (code) {callback(stderr); return;}
exec('pkill VBox', function (stderr, stdout, code) {
if (code) {callback(stderr); return;}
callback();
});
});
});
},
vmState: function (name, callback) {
exec(this.command() + ' showvminfo ' + name + ' --machinereadable', function (stderr, stdout, code) {
if (code) { callback(stderr); return; }
var match = stdout.match(/VMState="(\w+)"/);
if (!match) {
callback('Could not parse VMState');
return;
}
callback(null, match[1]);
});
},
deleteVM:function (name, callback) {
VirtualBox.vmState(name, function (err, state) {
// No VM found
if (err) { callback(null, false); return; }
VirtualBox.exec('controlvm ' + name + ' acpipowerbutton', function (stderr, stdout, code) {
if (code) { callback(stderr, false); return; }
var state = null;
async.until(function () {
return state === 'poweroff';
}, function (callback) {
VirtualBox.vmState(name, function (err, newState) {
if (err) { callback(err); return; }
state = newState;
setTimeout(callback, 250);
});
}, function (err) {
VirtualBox.exec('unregistervm ' + name + ' --delete', function (stderr, stdout, code) {
if (code) { callback(err); return; }
callback();
});
});
});
});
}
};
module.exports = VirtualBox;

View File

@ -10,7 +10,11 @@ var app = require('app');
var BrowserWindow = require('browser-window'); var BrowserWindow = require('browser-window');
var ipc = require('ipc'); var ipc = require('ipc');
var argv = require('minimist')(process.argv.slice(2)); var argv = require('minimist')(process.argv);
if (argv.test) {
console.log('Running tests');
}
app.on('activate-with-no-open-windows', function () { app.on('activate-with-no-open-windows', function () {
if (mainWindow) { if (mainWindow) {
@ -22,17 +26,22 @@ app.on('activate-with-no-open-windows', function () {
app.on('ready', function() { app.on('ready', function() {
// Create the browser window. // Create the browser window.
var windowOptions = { var windowOptions = {
width: 960, width: 1200,
height: 640, height: 800,
resizable: true, resizable: true,
frame: false, frame: true,
'web-preferences': { 'web-preferences': {
'web-security': false 'web-security': false
} }
}; };
mainWindow = new BrowserWindow(windowOptions); mainWindow = new BrowserWindow(windowOptions);
mainWindow.hide(); mainWindow.hide();
if (argv.test) {
mainWindow.loadUrl('file://' + __dirname + '/../build/specs.html');
} else{
mainWindow.loadUrl('file://' + __dirname + '/../build/index.html'); mainWindow.loadUrl('file://' + __dirname + '/../build/index.html');
}
process.on('uncaughtException', app.quit); process.on('uncaughtException', app.quit);
@ -51,6 +60,8 @@ app.on('ready', function() {
mainWindow.show(); mainWindow.show();
mainWindow.focus(); mainWindow.focus();
mainWindow.setTitle('');
// Auto Updates // Auto Updates
autoUpdater.setFeedUrl('https://updates.kitematic.com/releases/latest?version=' + app.getVersion()); autoUpdater.setFeedUrl('https://updates.kitematic.com/releases/latest?version=' + app.getVersion());

View File

@ -24,8 +24,10 @@ var packagejson = require('./package.json');
var http = require('http'); var http = require('http');
var dependencies = Object.keys(packagejson.dependencies); var dependencies = Object.keys(packagejson.dependencies);
var devDependencies = Object.keys(packagejson.devDependencies);
var options = { var options = {
dev: process.argv.indexOf('release') === -1, dev: process.argv.indexOf('release') === -1,
test: process.argv.indexOf('test') !== -1,
filename: 'Kitematic.app', filename: 'Kitematic.app',
name: 'Kitematic', name: 'Kitematic',
signing_identity: process.env.XCODE_SIGNING_IDENTITY signing_identity: process.env.XCODE_SIGNING_IDENTITY
@ -36,7 +38,11 @@ gulp.task('js', function () {
entries: ['./app/main.js'], // Only need initial file, browserify finds the rest entries: ['./app/main.js'], // Only need initial file, browserify finds the rest
transform: [reactify], // We want to convert JSX to normal javascript transform: [reactify], // We want to convert JSX to normal javascript
debug: options.dev, // Gives us sourcemapping debug: options.dev, // Gives us sourcemapping
ignoreMissing: true, builtins: false,
commondir: false,
insertGlobals: false,
detectGlobals: false,
bundleExternal: false,
cache: {}, packageCache: {}, fullPaths: options.dev // Requirement of watchify cache: {}, packageCache: {}, fullPaths: options.dev // Requirement of watchify
}); });
@ -45,6 +51,10 @@ gulp.task('js', function () {
bundler.external(dep); bundler.external(dep);
}); });
devDependencies.forEach(function (dep) {
bundler.external(dep);
});
bundler.external('./app'); bundler.external('./app');
var bundle = function () { var bundle = function () {
@ -53,7 +63,7 @@ gulp.task('js', function () {
.pipe(source('main.js')) .pipe(source('main.js'))
.pipe(gulpif(!options.dev, streamify(uglify()))) .pipe(gulpif(!options.dev, streamify(uglify())))
.pipe(gulp.dest(options.dev ? './build' : './dist/osx/' + options.filename + '/Contents/Resources/app/build')) .pipe(gulp.dest(options.dev ? './build' : './dist/osx/' + options.filename + '/Contents/Resources/app/build'))
.pipe(gulpif(options.dev, livereload())); .pipe(gulpif(options.dev && !options.test, livereload()));
}; };
if (options.dev) { if (options.dev) {
@ -76,25 +86,30 @@ gulp.task('specs', function () {
bundler.external(dep); bundler.external(dep);
}); });
var bundle = function () { devDependencies.forEach(function (dep) {
bundler.external(dep);
});
bundler.external('./app');
bundler.bundle() bundler.bundle()
.on('error', gutil.log) .on('error', gutil.log)
.pipe(source('specs.js')) .pipe(source('specs.js'))
.pipe(gulp.dest('./build')); .pipe(gulp.dest('./build'));
};
bundle(); gulp.src('./specs/specs.html')
.pipe(gulp.dest('./build'));
}); });
gulp.task('images', function() { gulp.task('images', function() {
return gulp.src('./app/images/**') return gulp.src('./app/images/*')
.pipe(imagemin({ .pipe(imagemin({
progressive: true, progressive: true,
interlaced: true, interlaced: true,
svgoPlugins: [{removeViewBox: false}] svgoPlugins: [{removeViewBox: false}]
})) }))
.pipe(gulp.dest(options.dev ? './build' : './dist/osx/' + options.filename + '/Contents/Resources/app/build')) .pipe(gulp.dest(options.dev ? './build' : './dist/osx/' + options.filename + '/Contents/Resources/app/build'))
.pipe(gulpif(options.dev, livereload())); .pipe(gulpif(options.dev && !options.test, livereload()));
}); });
gulp.task('styles', function () { gulp.task('styles', function () {
@ -106,7 +121,7 @@ gulp.task('styles', function () {
.pipe(gulp.dest(options.dev ? './build' : './dist/osx/' + options.filename + '/Contents/Resources/app/build')) .pipe(gulp.dest(options.dev ? './build' : './dist/osx/' + options.filename + '/Contents/Resources/app/build'))
.pipe(gulpif(!options.dev, cssmin())) .pipe(gulpif(!options.dev, cssmin()))
.pipe(concat('main.css')) .pipe(concat('main.css'))
.pipe(gulpif(options.dev, livereload())); .pipe(gulpif(options.dev && !options.test, livereload()));
}); });
gulp.task('download', function (cb) { gulp.task('download', function (cb) {
@ -119,7 +134,7 @@ gulp.task('download', function (cb) {
gulp.task('copy', function () { gulp.task('copy', function () {
gulp.src('./app/index.html') gulp.src('./app/index.html')
.pipe(gulp.dest(options.dev ? './build' : './dist/osx/' + options.filename + '/Contents/Resources/app/build')) .pipe(gulp.dest(options.dev ? './build' : './dist/osx/' + options.filename + '/Contents/Resources/app/build'))
.pipe(gulpif(options.dev, livereload())); .pipe(gulpif(options.dev && !options.test, livereload()));
}); });
gulp.task('dist', function (cb) { gulp.task('dist', function (cb) {
@ -144,7 +159,7 @@ gulp.task('dist', function (cb) {
filename: options.filename, filename: options.filename,
name: options.name, name: options.name,
version: packagejson.version, version: packagejson.version,
bundle: 'com.kitematic.kitematic' bundle: 'com.kitematic.app'
} }
})); }));
@ -181,10 +196,18 @@ gulp.task('release', function () {
runSequence('download', 'dist', ['copy', 'images', 'js', 'styles'], 'sign', 'zip'); runSequence('download', 'dist', ['copy', 'images', 'js', 'styles'], 'sign', 'zip');
}); });
gulp.task('test', ['download', 'copy', 'js', 'images', 'styles', 'specs'], function () {
var env = process.env;
env.NODE_ENV = 'development';
gulp.src('').pipe(shell(['./cache/Atom.app/Contents/MacOS/Atom . --test'], {
env: env
}));
});
gulp.task('default', ['download', 'copy', 'js', 'images', 'styles'], function () { gulp.task('default', ['download', 'copy', 'js', 'images', 'styles'], function () {
gulp.watch('./app/**/*.html', ['copy']); gulp.watch('./app/**/*.html', ['copy']);
gulp.watch('./app/images/**', ['images']); gulp.watch('./app/images/**', ['images']);
gulp.watch('./app/styles/**/*.less', ['styles']); gulp.watch('./app/styles/**', ['styles']);
livereload.listen(); livereload.listen();
@ -194,7 +217,3 @@ gulp.task('default', ['download', 'copy', 'js', 'images', 'styles'], function ()
env: env env: env
})); }));
}); });
gulp.task('test', function () {
return gulp.src('./app/__tests__').pipe(jest());
});

View File

@ -37,7 +37,8 @@
"react": "^0.12.1", "react": "^0.12.1",
"request": "2.42.0", "request": "2.42.0",
"request-progress": "0.3.1", "request-progress": "0.3.1",
"tar": "0.1.20" "tar": "0.1.20",
"retina.js": "^1.1.0"
}, },
"devDependencies": { "devDependencies": {
"browserify": "^6.2.0", "browserify": "^6.2.0",
@ -60,6 +61,7 @@
"gulp-uglify": "^0.3.1", "gulp-uglify": "^0.3.1",
"gulp-util": "^3.0.0", "gulp-util": "^3.0.0",
"jasmine-tagged": "^1.1.2", "jasmine-tagged": "^1.1.2",
"object-assign": "^2.0.0",
"reactify": "^0.15.2", "reactify": "^0.15.2",
"run-sequence": "^1.0.2", "run-sequence": "^1.0.2",
"vinyl-source-stream": "^0.1.1", "vinyl-source-stream": "^0.1.1",

5
script/test Executable file
View File

@ -0,0 +1,5 @@
#!/bin/bash
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
source $DIR/env
gulp test

View File

@ -1,11 +1,11 @@
var App = require('./../app/App.js'); var Containers = require('./../app/Containers.react.js');
var TestUtils = require('react/addons').TestUtils; var TestUtils = require('react/addons').TestUtils;
var jasmine = require('jasmine-node');
describe("App", function() { describe('Containers', function() {
it('should be wrapped with a div', function() {
it("should be wrapped with a div", function() { // var app = TestUtils.renderIntoDocument(App());
var app = TestUtils.renderIntoDocument(App()); expect(true).toEqual(true);
expect(app.getDOMNode().tagName).toEqual('DIV');
}); });
}); });

9
specs/specs.html Normal file
View File

@ -0,0 +1,9 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="main.css"/>
</head>
<body>
<script src="specs.js"></script>
</body>
</html>