Skip to content

Commit 1635d68

Browse files
authored
Merge pull request #200 from pwfisher/master
ember fastboot --watch
2 parents 664fe11 + 276793c commit 1635d68

6 files changed

Lines changed: 516 additions & 66 deletions

File tree

index.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,10 @@ module.exports = {
106106
});
107107

108108
return fastbootBuild.toTree();
109-
}
109+
},
110+
111+
postBuild: function() {
112+
process.emit('SIGHUP');
113+
},
110114

111115
};

lib/commands/fastboot.js

Lines changed: 44 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
1-
var RSVP = require('rsvp');
2-
var exec = RSVP.denodeify(require('child_process').exec);
1+
'use strict';
2+
3+
const RSVP = require('rsvp');
4+
const getPort = RSVP.denodeify(require('portfinder').getPort);
5+
const ServerTask = require('../tasks/fastboot-server');
6+
const SilentError = require('silent-error');
7+
8+
const blockForever = () => (new RSVP.Promise(() => {}));
9+
const noop = function() { };
310

411
module.exports = {
512
name: 'fastboot',
6-
description: 'Runs a server to render your app using FastBoot.',
13+
description: 'Builds and serves your FastBoot app, rebuilding on file changes.',
714

815
availableOptions: [
916
{ name: 'build', type: Boolean, default: true },
17+
{ name: 'watch', type: Boolean, default: true, aliases: ['w'] },
1018
{ name: 'environment', type: String, default: 'development', aliases: ['e',{'dev' : 'development'}, {'prod' : 'production'}] },
1119
{ name: 'serve-assets', type: Boolean, default: false },
1220
{ name: 'host', type: String, default: '::' },
@@ -15,75 +23,48 @@ module.exports = {
1523
{ name: 'assets-path', type: String, default: 'dist' }
1624
],
1725

18-
runCommand: function(appName, options) {
19-
var commandOptions = this.commandOptions;
20-
var outputPath = commandOptions.outputPath;
21-
var assetsPath = commandOptions.assetsPath;
22-
var ui = this.ui;
23-
24-
ui.writeLine("Installing FastBoot npm dependencies");
25-
26-
return exec('npm install', { cwd: outputPath })
27-
.then(function() {
28-
var fastbootMiddleware = require('fastboot-express-middleware');
29-
var RSVP = require('rsvp');
30-
var express = require('express');
31-
32-
var middleware = fastbootMiddleware(outputPath);
33-
34-
var app = express();
35-
36-
if (commandOptions.serveAssets) {
37-
app.get('/', middleware);
38-
app.use(express.static(assetsPath));
39-
}
40-
41-
app.get('/*', middleware);
42-
43-
app.use(function(req, res) {
44-
res.sendStatus(404);
45-
});
46-
47-
var listener = app.listen(options.port, options.host, function() {
48-
var host = listener.address().address;
49-
var port = listener.address().port;
50-
var family = listener.address().family;
26+
blockForever,
27+
getPort,
28+
ServerTask,
5129

52-
if (family === 'IPv6') { host = '[' + host + ']'; }
30+
run(options) {
31+
const runBuild = () => this.runBuild(options);
32+
const runServer = () => this.runServer(options);
33+
const blockForever = this.blockForever;
5334

54-
ui.writeLine('Ember FastBoot running at http://' + host + ":" + port);
55-
});
35+
return this.checkPort(options)
36+
.then(runServer) // starts on postBuild SIGHUP
37+
.then(options.build ? runBuild : noop)
38+
.then(blockForever);
39+
},
5640

57-
// Block forever
58-
return new RSVP.Promise(function() { });
59-
});
41+
runServer(options) {
42+
const ServerTask = this.ServerTask;
43+
const serverTask = new ServerTask({
44+
ui: this.ui,
45+
});
46+
return serverTask.run(options);
6047
},
6148

62-
triggerBuild: function(commandOptions) {
63-
var BuildTask = this.tasks.Build;
64-
var buildTask = new BuildTask({
49+
runBuild(options) {
50+
const BuildTask = options.watch ? this.tasks.BuildWatch : this.tasks.Build;
51+
const buildTask = new BuildTask({
6552
ui: this.ui,
6653
analytics: this.analytics,
67-
project: this.project
54+
project: this.project,
6855
});
69-
70-
return buildTask.run(commandOptions);
56+
buildTask.run(options); // no return, BuildWatch.run blocks forever
7157
},
7258

73-
run: function(options) {
74-
this.commandOptions = options;
75-
76-
var runCommand = function() {
77-
var appName = process.env.EMBER_CLI_FASTBOOT_APP_NAME || this.project.name();
78-
79-
return this.runCommand(appName, options);
80-
}.bind(this);
81-
82-
if (options.build) {
83-
return this.triggerBuild(options)
84-
.then(runCommand);
85-
}
59+
checkPort(options) {
60+
return this.getPort({ port: options.port, host: options.host })
61+
.then((foundPort) => {
62+
if (options.port !== foundPort && options.port !== 0) {
63+
const message = `Port ${options.port} is already in use.`;
64+
return Promise.reject(new SilentError(message));
65+
}
66+
options.port = foundPort;
67+
});
68+
},
8669

87-
return runCommand();
88-
}
8970
};

lib/tasks/fastboot-server.js

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
'use strict';
2+
3+
const RSVP = require('rsvp');
4+
const CoreObject = require('core-object');
5+
const debug = require('debug')('ember-cli-fastboot/server');
6+
const exec = RSVP.denodeify(require('child_process').exec);
7+
const http = require('http');
8+
const path = require('path');
9+
10+
module.exports = CoreObject.extend({
11+
12+
exec,
13+
http,
14+
httpServer: null,
15+
nextSocketId: 0,
16+
require,
17+
restartAgain: false,
18+
restartPromise: null,
19+
sockets: {},
20+
21+
run(options) {
22+
debug('run');
23+
const restart = () => this.restart(options);
24+
process.on('SIGHUP', restart);
25+
},
26+
27+
start(options) {
28+
debug('start');
29+
this.ui.writeLine('Installing FastBoot npm dependencies');
30+
31+
return this.exec('npm install', { cwd: options.outputPath })
32+
.then(() => {
33+
const middleware = this.require('fastboot-express-middleware')(options.outputPath);
34+
const express = this.require('express');
35+
const app = express();
36+
37+
if (options.serveAssets) {
38+
app.get('/', middleware);
39+
app.use(express.static(options.assetsPath));
40+
}
41+
app.get('/*', middleware);
42+
app.use((req, res) => res.sendStatus(404));
43+
44+
this.httpServer = this.http.createServer(app);
45+
46+
// Track open sockets for fast restart
47+
this.httpServer.on('connection', (socket) => {
48+
const socketId = this.nextSocketId++;
49+
debug(`open socket ${socketId}`);
50+
this.sockets[socketId] = socket;
51+
socket.on('close', () => {
52+
debug(`close socket ${socketId}`);
53+
delete this.sockets[socketId];
54+
});
55+
});
56+
57+
this.httpServer.listen(options.port, options.host, () => {
58+
const o = this.httpServer.address();
59+
const port = o.port;
60+
const family = o.family;
61+
let host = o.address;
62+
if (family === 'IPv6') { host = `[${host}]`; }
63+
this.ui.writeLine(`Ember FastBoot running at http://${host}:${port}`);
64+
});
65+
});
66+
},
67+
68+
stop() {
69+
debug('stop');
70+
return new RSVP.Promise((resolve, reject) => {
71+
if (!this.httpServer) { return resolve(); }
72+
73+
// Stop accepting new connections
74+
this.httpServer.close((err) => {
75+
debug('close', Object.keys(this.sockets));
76+
if (err) { return reject(err); }
77+
this.httpServer = null;
78+
resolve();
79+
});
80+
81+
// Force close existing connections
82+
Object.keys(this.sockets).forEach(k => this.sockets[k].destroy());
83+
});
84+
},
85+
86+
restart(options) {
87+
if (this.restartPromise) {
88+
debug('schedule immediate restart');
89+
this.restartAgain = true;
90+
return;
91+
}
92+
debug('restart');
93+
this.restartPromise = this.stop()
94+
.then(() => this.clearRequireCache(options.outputPath))
95+
.then(() => this.start(options))
96+
.catch(e => this.ui.writeLine(e))
97+
.finally(() => {
98+
this.restartPromise = null;
99+
if (this.restartAgain) {
100+
debug('restart again')
101+
this.restartAgain = false;
102+
this.restart(options);
103+
}
104+
});
105+
return this.restartPromise;
106+
},
107+
108+
clearRequireCache: function (serverRoot) {
109+
debug('clearRequireCache');
110+
let absoluteServerRoot = path.resolve(serverRoot);
111+
if (absoluteServerRoot.slice(-1) !== path.sep) {
112+
absoluteServerRoot += path.sep;
113+
}
114+
Object.keys(require.cache).forEach(function (key) {
115+
if (key.indexOf(absoluteServerRoot) === 0) {
116+
delete require.cache[key];
117+
}
118+
});
119+
},
120+
121+
});

package.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"test:precook": "node node_modules/ember-cli-addon-tests/scripts/precook-node-modules.js"
1515
},
1616
"engines": {
17-
"node": ">= 0.10.0"
17+
"node": ">= 4.0.0"
1818
},
1919
"author": "Tom Dale & Yehuda Katz <tomhuda@tilde.io>",
2020
"license": "MIT",
@@ -23,7 +23,9 @@
2323
"chai": "^2.2.0",
2424
"chai-fs": "peterkc/chai-fs#31deec5232297e2887a2ffb39b9081364c8e78e1",
2525
"chalk": "^1.1.1",
26+
"core-object": "2.0.1",
2627
"cpr": "^1.1.1",
28+
"debug": "^2.2.0",
2729
"ember-cli": "^2.5.0",
2830
"ember-cli-addon-tests": "^0.3.0",
2931
"ember-cli-app-version": "0.5.0",
@@ -35,6 +37,7 @@
3537
"ember-cli-inject-live-reload": "^1.3.1",
3638
"ember-cli-qunit": "^1.0.0",
3739
"ember-cli-release": "^1.0.0-beta.1",
40+
"ember-cli-string-utils": "^1.0.0",
3841
"ember-cli-uglify": "^1.2.0",
3942
"ember-data": "1.13.8",
4043
"ember-export-application-global": "^1.0.3",
@@ -47,6 +50,7 @@
4750
"loader.js": "^4.0.1",
4851
"mocha": "^2.2.4",
4952
"request": "^2.55.0",
53+
"sinon": "^1.17.4",
5054
"symlink-or-copy": "^1.0.1",
5155
"temp": "^0.8.3",
5256
"walk-sync": "^0.2.5"
@@ -72,6 +76,8 @@
7276
"fastboot-filter-initializers": "0.0.2",
7377
"lodash.defaults": "^4.0.1",
7478
"lodash.uniq": "^4.2.0",
75-
"rsvp": "^3.0.16"
79+
"portfinder": "^1.0.3",
80+
"rsvp": "^3.0.16",
81+
"silent-error": "^1.0.0"
7682
}
7783
}

0 commit comments

Comments
 (0)