Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pass watchedNodes to Watcher/WatcherAdapter #403

Merged
merged 6 commits into from
May 20, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions lib/builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,12 @@ module.exports = class Builder extends EventEmitter {
return nodeWrapper;
}

get watchedSourceNodeWrappers() {
return this.nodeWrappers.filter(nw => {
return nw.nodeInfo.nodeType === 'source' && nw.nodeInfo.watched;
});
}

checkInputPathsExist() {
// We might consider checking this.unwatchedPaths as well.
for (let i = 0; i < this.watchedPaths.length; i++) {
Expand Down
15 changes: 11 additions & 4 deletions lib/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,11 @@ module.exports = function broccoliCLI(args) {
const builder = getBuilder(options);
const Watcher = getWatcher(options);
const outputDir = options.outputPath;
const watcher = new Watcher(builder, buildWatcherOptions(options));
const watcher = new Watcher(
builder,
builder.watchedSourceNodeWrappers,
buildWatcherOptions(options)
);

if (outputDir) {
try {
Expand All @@ -62,8 +66,7 @@ module.exports = function broccoliCLI(args) {
watcher.on('buildSuccess', () => outputTree.sync());
}

const server = broccoli.server.serve(watcher, options.host, parseInt(options.port, 10));
actionPromise = (server && server.closingPromise) || Promise.resolve();
actionPromise = broccoli.server.serve(watcher, options.host, parseInt(options.port, 10));
});

program
Expand Down Expand Up @@ -112,7 +115,11 @@ module.exports = function broccoliCLI(args) {
const builder = getBuilder(options);
const Watcher = getWatcher(options);
const outputTree = new TreeSync(builder.outputPath, outputDir);
const watcher = new Watcher(builder, buildWatcherOptions(options));
const watcher = new Watcher(
builder,
builder.watchedSourceNodeWrappers,
buildWatcherOptions(options)
);

watcher.on('buildSuccess', () => {
outputTree.sync();
Expand Down
6 changes: 2 additions & 4 deletions lib/dummy-watcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,14 @@ module.exports = class Watcher extends EventEmitter {
super();
this.builder = builder;
this.currentBuild = null;
this._lifetimeDeferred = null;
}

start() {
let lifetime = (this._lifetimeDeferred = {});
lifetime.promise = new Promise((resolve, reject) => {
lifetime.resolve = resolve;
lifetime.reject = reject;
});
}

start() {
this.currentBuild = this.builder.build();
this.currentBuild
.then(() => this.emit('buildSuccess'))
Expand Down
10 changes: 6 additions & 4 deletions lib/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@ module.exports = {

onBuildFailure(err) {
console.log('Built with error:');
console.log(err.message);
if (!err.broccoliPayload || !err.broccoliPayload.location.file) {
if (err !== null && typeof err === 'object') {
console.log(err.message);
if (!err.broccoliPayload || !err.broccoliPayload.location.file) {
console.log('');
console.log(err.stack);
}
console.log('');
console.log(err.stack);
}
console.log('');
},
};
137 changes: 91 additions & 46 deletions lib/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,67 +2,112 @@

const promiseFinally = require('promise.prototype.finally');
const http = require('http');

const messages = require('./messages');
const middleware = require('./middleware');
const EventEmitter = require('events');

class Server extends EventEmitter {
constructor(watcher, host, port, connect = require('connect')) {
super();

this._watcher = watcher;
this._builder = watcher.builder;
this._host = host;
this._port = parseInt(port, 10);
this._connect = connect;
this._url = `http://${this._host}:${this._port}`;
this.app = this.http = null;
this._boundStop = this.stop.bind(this);
this._started = false;

if (watcher.constructor.name !== 'Watcher') {
throw new Error('Expected Watcher instance');
}

exports.serve = function serve(watcher, host, port, _connect) {
if (watcher.constructor.name !== 'Watcher') throw new Error('Expected Watcher instance');
if (typeof host !== 'string') throw new Error('Expected host to bind to (e.g. "localhost")');
if (typeof port !== 'number' || port !== port)
throw new Error('Expected port to bind to (e.g. 4200)');
if (typeof host !== 'string') {
throw new Error('Expected host to bind to (e.g. "localhost")');
}

let connect = arguments.length > 3 ? _connect : require('connect');
if (typeof port !== 'number' || port !== port) {
throw new Error('Expected port to bind to (e.g. 4200)');
}
}

const server = {
onBuildSuccessful: () => messages.onBuildSuccess(watcher.builder),
cleanupAndExit,
};
start() {
if (this._started) {
throw new Error('Watcher.prototype.start() must not be called more than once');
}

console.log(`Serving on http://${host}:${port}\n`);
const promise = new Promise((resolve, reject) => {
this.app = this._connect().use(middleware(this._watcher));

server.watcher = watcher;
server.builder = server.watcher.builder;
this.http = http.createServer(this.app);
this.http.listen(this._port, this._host);

server.app = connect().use(middleware(server.watcher));
this.http.on('listening', () => {
console.log(`Serving on ${this._url}\n`);
resolve(this._watcher.start());
});

server.http = http.createServer(server.app);
server.http.listen(parseInt(port, 10), host);
server.http.on('error', error => {
if (error.code !== 'EADDRINUSE') {
throw error;
}
this.http.on('error', error => {
if (error.code !== 'EADDRINUSE') {
throw error;
}

let message = `Oh snap 😫. It appears a server is already running on http://${host}:${port}\n`;
message += `Are you perhaps already running serve in another terminal window?\n`;
let message = `Oh snap 😫. It appears a server is already running on ${this._url}\n`;
message += `Are you perhaps already running serve in another terminal window?\n`;
reject(new Error(message));
});

console.error(message);
process.exit(1);
});
process.addListener('SIGINT', this._boundStop);
process.addListener('SIGTERM', this._boundStop);

// We register these so the 'exit' handler removing temp dirs is called
function cleanupAndExit() {
return server.watcher.quit();
}
this._watcher.on('buildSuccess', () => {
this.emit('buildSuccess');
messages.onBuildSuccess(this._builder);
});

this._watcher.on('buildFailure', () => {
this.emit('buildFailure');
messages.onBuildFailure();
});
});

process.on('SIGINT', cleanupAndExit);
process.on('SIGTERM', cleanupAndExit);
return promiseFinally(promise, () => this.stop());
}

server.watcher.on('buildSuccess', () => server.onBuildSuccessful());
server.watcher.on('buildFailure', messages.onBuildFailure);
_detachListeners() {
process.removeListener('SIGINT', this._boundStop);
process.removeListener('SIGTERM', this._boundStop);
}

server.closingPromise = promiseFinally(
server.watcher.start().catch(err => console.log((err && err.stack) || err)),
() => {
server.builder.cleanup();
server.http.close();
process.exit(0);
stop() {
this._detachListeners();
if (this.http) {
this.http.close();
}
).catch(err => {
console.log('Cleanup error:');
console.log((err && err.stack) || err);
process.exit(1);
});

return server;
return this._watcher.quit().then(() => this._builder.cleanup());
}
}

exports.serve = function serve(
watcher,
host,
port,
_connect = require('connect'),
_process = process
) {
const server = new Server(watcher, host, port, _connect);

return server
.start()
.then(() => _process.exit(0))
.catch(err => {
console.log('Broccoli Cleanup error:');
console.log((err && err.stack) || err);
_process.exit(1);
});
};

exports.Server = Server;
36 changes: 19 additions & 17 deletions lib/watcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,30 @@ const logger = require('heimdalljs-logger')('broccoli:watcher');
// principle.

module.exports = class Watcher extends EventEmitter {
constructor(builder, options) {
constructor(builder, watchedNodes = [], options = {}) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the watched nodes are required, why do we default them?

super();
this.options = options || {};
this.options = options;
if (this.options.debounce == null) {
this.options.debounce = 100;
}
this.builder = builder;
this.watcherAdapter =
this.options.watcherAdapter || new WatcherAdapter(this.options.saneOptions);
this.options.watcherAdapter ||
new WatcherAdapter(watchedNodes || [], this.options.saneOptions);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we default watchedNodes twice?


this.currentBuild = null;
this._rebuildScheduled = false;
this._ready = false;
this._quittingPromise = null;
this._lifetimeDeferred = null;
this._lifetime = null;
}

start() {
if (this._lifetimeDeferred != null) {
if (this._lifetime != null) {
throw new Error('Watcher.prototype.start() must not be called more than once');
}

let lifetime = (this._lifetimeDeferred = {});
let lifetime = (this._lifetime = {});
lifetime.promise = new Promise((resolve, reject) => {
lifetime.resolve = resolve;
lifetime.reject = reject;
Expand All @@ -41,11 +43,7 @@ module.exports = class Watcher extends EventEmitter {
this.watcherAdapter.on('error', this._error.bind(this));
this.currentBuild = Promise.resolve()
.then(() => {
let watchedSourceNodes = this.builder.nodeWrappers.filter(nw => {
return nw.nodeInfo.nodeType === 'source' && nw.nodeInfo.watched;
});

return this.watcherAdapter.watch(watchedSourceNodes);
return this.watcherAdapter.watch();
})
.then(() => {
logger.debug('ready');
Expand All @@ -55,7 +53,7 @@ module.exports = class Watcher extends EventEmitter {
.catch(err => this._error(err))
.then(() => this.currentBuild);

return this._lifetimeDeferred.promise;
return this._lifetime.promise;
}

_change(event, filePath, root) {
Expand Down Expand Up @@ -127,7 +125,7 @@ module.exports = class Watcher extends EventEmitter {
this.emit('error', err);
return this._quit()
.catch(() => {})
.then(() => this._lifetimeDeferred.reject(err));
.then(() => this._lifetime.reject(err));
}

quit() {
Expand All @@ -136,10 +134,14 @@ module.exports = class Watcher extends EventEmitter {
return this._quittingPromise;
}

return this._quit().then(
() => this._lifetimeDeferred.resolve(),
err => this._lifetimeDeferred.reject(err)
);
let quitting = this._quit();

if (this._lifetime) {
this._lifetime.resolve(quitting);
return this._lifetime.promise;
} else {
return quitting;
}
}

_quit() {
Expand Down
26 changes: 18 additions & 8 deletions lib/watcher_adapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
const EventEmitter = require('events').EventEmitter;
const sane = require('sane');
const logger = require('heimdalljs-logger')('broccoli:watcherAdapter');
const SourceNode = require('./wrappers/source-node');

function defaultFilterFunction(name) {
return /^[^.]/.test(name);
Expand All @@ -18,20 +19,29 @@ function bindFileEvent(adapter, watcher, node, event) {
}

module.exports = class WatcherAdapter extends EventEmitter {
constructor(options) {
constructor(watchedNodes = [], options) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO, we should just require an arg here. This defaulting is a bit odd (e.g. it only supports new WatcherAdapter(undefined, { /* options here */ })).

super();
if (!Array.isArray(watchedNodes)) {
throw new TypeError(
`WatcherAdapter's first argument must be an array of SourceNodeWrapper nodes`
);
}
for (const node of watchedNodes) {
if (!(node instanceof SourceNode)) {
throw new Error(`${node} is not a SourceNode`);
}
if (node.nodeInfo.watched !== true) {
throw new Error(`'${node.nodeInfo.sourceDirectory}' is not watched`);
}
}
this.watchedNodes = watchedNodes;
this.options = options || {};
this.options.filter = this.options.filter || defaultFilterFunction;
this.watchers = [];
}

watch(watchedSourceNodes) {
if (!Array.isArray(watchedSourceNodes)) {
throw new TypeError(
`WatcherAdapter#watch's first argument must be an array of WatchedDir nodes`
);
}
let watchers = watchedSourceNodes.map(node => {
watch() {
let watchers = this.watchedNodes.map(node => {
const watchedPath = node.nodeInfo.sourceDirectory;
const watcher = new sane(watchedPath, this.options);
this.watchers.push(watcher);
Expand Down
11 changes: 11 additions & 0 deletions test/builder_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,17 @@ describe('Builder', function() {
/test\/fixtures\/basic\/foo\.txt: Not a directory/
);
});

it('returns only watched SourceNodeWrappers from watchedSourceNodeWrappers', function() {
const source1 = new broccoliSource.WatchedDir('test/fixtures/basic/');
const source2 = new broccoliSource.UnwatchedDir('test/fixtures/empty/');
builder = new Builder(new plugins.Merge([source1, source2]));

const watchedSourceNodeWrappers = builder.watchedSourceNodeWrappers;

expect(watchedSourceNodeWrappers).length.to.be(1);
expect(watchedSourceNodeWrappers[0].node).to.equal(source1);
});
});

describe('error handling in constructor', function() {
Expand Down
Loading