Skip to content

Commit 132f92f

Browse files
committed
Complete ember fastboot --watch implementation
unit tests for lib/commands/fastboot
1 parent e21a8fe commit 132f92f

5 files changed

Lines changed: 265 additions & 83 deletions

File tree

index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,8 @@ module.exports = {
108108
return fastbootBuild.toTree();
109109
},
110110

111-
outputReady: function() {
112-
process.emit('SIGUSR1');
111+
postBuild: function() {
112+
process.emit('SIGHUP');
113113
},
114114

115115
};

lib/commands/fastboot.js

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

410
module.exports = {
511
name: 'fastboot',
6-
description: 'Runs a server to render your app using FastBoot.',
12+
description: 'Builds and serves your FastBoot app, rebuilding on file changes.',
713

814
availableOptions: [
915
{ name: 'build', type: Boolean, default: true },
10-
{ name: 'watch', type: Boolean, default: false, aliases: ['w'] },
16+
{ name: 'watch', type: Boolean, default: true, aliases: ['w'] },
1117
{ name: 'environment', type: String, default: 'development', aliases: ['e',{'dev' : 'development'}, {'prod' : 'production'}] },
1218
{ name: 'serve-assets', type: Boolean, default: false },
1319
{ name: 'host', type: String, default: '::' },
@@ -16,91 +22,48 @@ module.exports = {
1622
{ name: 'assets-path', type: String, default: 'dist' }
1723
],
1824

19-
startServer: function(options) {
20-
var ui = this.ui;
21-
var self = this;
22-
23-
if (this._fastBootListener) {
24-
this._fastBootListener.close();
25-
}
26-
27-
ui.writeLine('Installing FastBoot npm dependencies');
28-
29-
return exec('npm install', { cwd: options.outputPath })
30-
.then(function() {
31-
var fastbootMiddleware = require('fastboot-express-middleware');
32-
var RSVP = require('rsvp');
33-
var express = require('express');
34-
35-
var middleware = fastbootMiddleware(options.outputPath);
36-
37-
var app = express();
25+
blockForever,
26+
getPort,
27+
ServerTask,
3828

39-
if (options.serveAssets) {
40-
app.get('/', middleware);
41-
app.use(express.static(options.assetsPath));
42-
}
43-
44-
app.get('/*', middleware);
45-
46-
app.use(function(req, res) {
47-
res.sendStatus(404);
48-
});
29+
run(options) {
30+
const runBuild = () => this.runBuild(options);
31+
const runServer = () => this.runServer(options);
32+
const blockForever = this.blockForever;
4933

50-
var listener = app.listen(options.port, options.host, function() {
51-
var host = listener.address().address;
52-
var port = listener.address().port;
53-
var family = listener.address().family;
54-
55-
if (family === 'IPv6') { host = '[' + host + ']'; }
34+
return this.checkPort(options)
35+
.then(runServer) // starts on postBuild SIGHUP
36+
.then(options.build ? runBuild : noop)
37+
.then(blockForever);
38+
},
5639

57-
ui.writeLine('Ember FastBoot running at http://' + host + ":" + port);
58-
self._fastBootListener = listener;
59-
});
60-
});
40+
runServer(options) {
41+
const ServerTask = this.ServerTask;
42+
const serverTask = new ServerTask({
43+
ui: this.ui,
44+
});
45+
return serverTask.run(options);
6146
},
6247

63-
triggerBuild: function(options) {
64-
var BuildTask = this.buildTaskFor(options);
65-
var buildTask = new BuildTask({
48+
runBuild(options) {
49+
const BuildTask = options.watch ? this.tasks.BuildWatch : this.tasks.Build;
50+
const buildTask = new BuildTask({
6651
ui: this.ui,
6752
analytics: this.analytics,
68-
project: this.project
53+
project: this.project,
6954
});
70-
71-
return buildTask.run(options);
55+
buildTask.run(options); // no return, BuildWatch.run blocks forever
7256
},
7357

74-
buildTaskFor: function(options) {
75-
if (options.watch) {
76-
return this.tasks.BuildWatch;
77-
} else {
78-
return this.tasks.Build;
79-
}
58+
checkPort(options) {
59+
return this.getPort({ port: options.port, host: options.host })
60+
.then((foundPort) => {
61+
if (options.port !== foundPort && options.port !== 0) {
62+
const message = `Port ${options.port} is already in use.`;
63+
return Promise.reject(new SilentError(message));
64+
}
65+
options.port = foundPort;
66+
});
8067
},
8168

82-
run: function(options) {
83-
var startServer = function() {
84-
this.startServer(options);
85-
}.bind(this);
86-
87-
var blockForever = function() {
88-
return new RSVP.Promise(function() { });
89-
};
90-
91-
process.on('SIGUSR1', startServer);
92-
93-
if (options.build) {
94-
return this.triggerBuild(options)
95-
.then(startServer)
96-
.then(blockForever);
97-
}
98-
99-
if (options.watch) {
100-
this.ui.writeWarnLine('The `watch` option is not supported when `build` is disabled.');
101-
}
102-
103-
return startServer()
104-
.then(blockForever);
105-
}
10669
};

lib/tasks/fastboot-server.js

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

package.json

Lines changed: 7 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",
@@ -74,6 +77,8 @@
7477
"lodash.defaults": "^4.0.1",
7578
"lodash.find": "^4.2.0",
7679
"lodash.uniq": "^4.2.0",
77-
"rsvp": "^3.0.16"
80+
"portfinder": "^1.0.3",
81+
"rsvp": "^3.0.16",
82+
"silent-error": "^1.0.0"
7883
}
7984
}

0 commit comments

Comments
 (0)