New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

broccoli

Package Overview
Dependencies
Maintainers
3
Versions
72
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

broccoli - npm Package Compare versions

Comparing version 1.1.4 to 2.0.0-beta.1

.eslintignore

17

CHANGELOG.md
# master
# 2.0.0-beta.1
* Drop Node 4 support.
* Drop Node 0.12 support.
* Add visualization support via heimdalljs.
* Add support for Node 10.
* Ensure that mid-build cancelation avoids extra work.
* Add `--overwrite` option to the command line interface which clobbers any
existing directory contents with the contents of the new build.
* Add `--cwd` option to the command line interface which allows customizing the
builders working directory (and where the `Brocfile.js` is looked up from).
* Add `--output-path` option to the command line interface.
* Add `--watch` option to `build` sub-command.
* Add `--no-watch` option to `serve` sub-command.
* Add `--watcher` option to allow configuration of the watcher to be used. Currently supported values are polling, watchman, node, events.
* General code cleanup and modernization.
# 1.1.4

@@ -4,0 +21,0 @@

797

lib/builder.js

@@ -1,16 +0,24 @@

'use strict'
'use strict';
var path = require('path')
var fs = require('fs')
var RSVP = require('rsvp')
var tmp = require('tmp')
var rimraf = require('rimraf')
var underscoreString = require('underscore.string')
var WatchedDir = require('broccoli-source').WatchedDir
var broccoliNodeInfo = require('broccoli-node-info')
const path = require('path');
const fs = require('fs');
const RSVP = require('rsvp');
const tmp = require('tmp');
const heimdall = require('heimdalljs');
const underscoreString = require('underscore.string');
const WatchedDir = require('broccoli-source').WatchedDir;
const broccoliNodeInfo = require('broccoli-node-info');
const NodeWrapper = require('./wrappers/node');
const TransformNodeWrapper = require('./wrappers/transform-node');
const SourceNodeWrapper = require('./wrappers/source-node');
const BuilderError = require('./errors/builder');
const NodeSetupError = require('./errors/node-setup');
const BuildError = require('./errors/build');
const CancelationRequest = require('./cancellation-request');
// Clean up left-over temporary directories on uncaught exception.
tmp.setGracefulCleanup()
tmp.setGracefulCleanup();
// For an explanation and reference of the API that we use to communicate with

@@ -20,14 +28,13 @@ // nodes (__broccoliFeatures__ and __broccoliGetInfo__), see

// Build a graph of nodes, referenced by its final output node. Example:
//
// var builder = new Builder(outputNode)
// const builder = new Builder(outputNode)
// builder.build()
// .then(function() {
// .then(() => {
// // Build output has been written to builder.outputPath
// })
// // To rebuild, call builder.build() repeatedly
// .finally(function() {
// .finally(() => {
// // Delete temporary directories
// builder.cleanup()
// return builder.cleanup()
// })

@@ -39,512 +46,400 @@ //

module.exports = Builder
function Builder(outputNode, options) {
if (options == null) options = {}
module.exports = class Builder {
constructor(outputNode, options) {
if (options == null) options = {};
this.outputNode = outputNode
this.tmpdir = options.tmpdir // can be null
this.outputNode = outputNode;
this.tmpdir = options.tmpdir; // can be null
this.unwatchedPaths = []
this.watchedPaths = []
this.unwatchedPaths = [];
this.watchedPaths = [];
// nodeWrappers store additional bookkeeping information, such as paths.
// This array contains them in topological (build) order.
this.nodeWrappers = []
// This populates this.nodeWrappers as a side effect
this.outputNodeWrapper = this.makeNodeWrapper(this.outputNode)
// nodeWrappers store additional bookkeeping information, such as paths.
// This array contains them in topological (build) order.
this.nodeWrappers = [];
// This populates this.nodeWrappers as a side effect
this.outputNodeWrapper = this.makeNodeWrapper(this.outputNode);
// Catching missing directories here helps prevent later errors when we set
// up the watcher.
this.checkInputPathsExist()
// Catching missing directories here helps prevent later errors when we set
// up the watcher.
this.checkInputPathsExist();
this.setupTmpDirs()
this.setupTmpDirs();
this.setupHeimdall();
this._cancelationRequest = undefined;
// Now that temporary directories are set up, we need to run the rest of the
// constructor in a try/catch block to clean them up if necessary.
try {
// Now that temporary directories are set up, we need to run the rest of the
// constructor in a try/catch block to clean them up if necessary.
try {
this.setupNodes();
this.outputPath = this.outputNodeWrapper.outputPath;
this.buildId = 0;
} catch (e) {
this.cleanup();
throw e;
}
}
this.setupNodes()
this.outputPath = this.outputNodeWrapper.outputPath
this.buildId = 0
// Trigger a (re)build.
//
// Returns a promise that resolves when the build has finished. If there is a
// build error, the promise is rejected with a Builder.BuildError instance.
// This method will never throw, and it will never be rejected with anything
// other than a BuildError.
build() {
if (this._cancelationRequest) {
return RSVP.Promise.reject(
new BuilderError('Cannot start a build if one is already running')
);
}
} catch (e) {
this.cleanup()
throw e
}
}
let promise = RSVP.Promise.resolve();
RSVP.EventTarget.mixin(Builder.prototype)
this.buildId++;
// Trigger a (re)build.
//
// Returns a promise that resolves when the build has finished. If there is a
// build error, the promise is rejected with a Builder.BuildError instance.
// This method will never throw, and it will never be rejected with anything
// other than a BuildError.
Builder.prototype.build = function() {
var self = this
this.buildId++
var promise = RSVP.resolve()
this.nodeWrappers.forEach(function(nw) {
// We use `.forEach` instead of `for` to close nested functions over `nw`
this.nodeWrappers.forEach(nw => {
// We use `.forEach` instead of `for` to close nested functions over `nw`
// Wipe all buildState objects at the beginning of the build
nw.buildState = {}
// Wipe all buildState objects at the beginning of the build
nw.buildState = {};
promise = promise
.then(function() {
promise = promise.then(() => {
// We use a nested .then/.catch so that the .catch can only catch errors
// from this node, but not from previous nodes.
return RSVP.resolve()
.then(function() {
self.trigger('beginNode', nw)
.then(() => this._cancelationRequest.throwIfRequested())
.then(() => this.trigger('beginNode', nw))
.then(() => nw.build())
.finally(() => {
if (this._cancelationRequest.isCancelled) {
return;
}
this.trigger('endNode', nw);
})
.then(function() {
return nw.build()
})
.finally(function() {
self.trigger('endNode', nw)
})
.catch(function(err) {
throw new BuildError(err, nw)
})
})
})
return promise
}
.then(() => this._cancelationRequest.throwIfRequested())
.catch(err => {
throw new BuildError(err, nw);
});
});
});
// Destructor-like method. Cleanup is synchronous at the moment, but in the
// future we might change it to return a promise.
Builder.prototype.cleanup = function() {
this.builderTmpDirCleanup()
}
this._cancelationRequest = new CancelationRequest(promise);
// This method recursively traverses the node graph and returns a nodeWrapper.
// The nodeWrapper graph parallels the node graph 1:1.
Builder.prototype.makeNodeWrapper = function(node, _stack) {
if (_stack == null) _stack = []
var self = this
return promise
.then(() => {
return this.outputNodeWrapper;
})
.then(outputNodeWrapper => {
this.buildHeimdallTree(outputNodeWrapper);
return outputNodeWrapper;
})
.finally(() => {
this._cancelationRequest = null;
});
}
// Dedupe nodes reachable through multiple paths
for (var i = 0; i < this.nodeWrappers.length; i++) {
if (this.nodeWrappers[i].originalNode === node) {
return this.nodeWrappers[i]
cancel() {
if (!this._cancelationRequest) {
// No current build, so no cancellation
return RSVP.Promise.resolve();
}
}
// Turn string nodes into WatchedDir nodes
var originalNode = node // keep original (possibly string) node around for deduping
if (typeof node === 'string') {
node = new WatchedDir(node, { annotation: 'string node' })
return this._cancelationRequest.cancel();
}
// Call node.__broccoliGetInfo__()
var nodeInfo
try {
nodeInfo = broccoliNodeInfo.getNodeInfo(node)
} catch (e) {
if (!(e instanceof broccoliNodeInfo.InvalidNodeError)) throw e
// We don't have the instantiation stack of an invalid node, so to aid
// debugging, we instead report its parent node
var messageSuffix = (_stack.length > 0) ?
'\nused as input node to ' + _stack[_stack.length-1].label +
_stack[_stack.length-1].formatInstantiationStackForTerminal()
: '\nused as output node'
throw new broccoliNodeInfo.InvalidNodeError(e.message + messageSuffix)
// Destructor-like method. Cleanup is synchronous at the moment, but in the
// future we might change it to return a promise.
cleanup() {
this.builderTmpDirCleanup();
}
// Compute label, like "Funnel (test suite)"
var label = nodeInfo.name
var labelExtras = []
if (nodeInfo.nodeType === 'source') labelExtras.push(nodeInfo.sourceDirectory)
if (nodeInfo.annotation != null) labelExtras.push(nodeInfo.annotation)
if (labelExtras.length > 0) label += ' (' + labelExtras.join('; ') + ')'
// This method recursively traverses the node graph and returns a nodeWrapper.
// The nodeWrapper graph parallels the node graph 1:1.
makeNodeWrapper(node, _stack) {
if (_stack == null) _stack = [];
// We start constructing the nodeWrapper here because we'll need the partial
// nodeWrapper for the _stack. Later we'll add more properties.
var nodeWrapper = nodeInfo.nodeType === 'transform' ?
new TransformNodeWrapper : new SourceNodeWrapper
nodeWrapper.nodeInfo = nodeInfo
nodeWrapper.originalNode = originalNode
nodeWrapper.node = node
nodeWrapper.label = label
// Detect cycles
for (i = 0; i < _stack.length; i++) {
if (_stack[i].node === originalNode) {
var cycleMessage = 'Cycle in node graph: '
for (var j = i; j < _stack.length; j++) {
cycleMessage += _stack[j].label + ' -> '
// Dedupe nodes reachable through multiple paths
for (let i = 0; i < this.nodeWrappers.length; i++) {
if (this.nodeWrappers[i].originalNode === node) {
return this.nodeWrappers[i];
}
cycleMessage += nodeWrapper.label
throw new BuilderError(cycleMessage)
}
}
// For 'transform' nodes, recurse into the input nodes; for 'source' nodes,
// record paths.
var inputNodeWrappers = []
if (nodeInfo.nodeType === 'transform') {
var newStack = _stack.concat([nodeWrapper])
inputNodeWrappers = nodeInfo.inputNodes.map(function(inputNode) {
return self.makeNodeWrapper(inputNode, newStack)
})
} else { // nodeType === 'source'
if (nodeInfo.watched) {
this.watchedPaths.push(nodeInfo.sourceDirectory)
} else {
this.unwatchedPaths.push(nodeInfo.sourceDirectory)
// Turn string nodes into WatchedDir nodes
const originalNode = node; // keep original (possibly string) node around for deduping
if (typeof node === 'string') {
node = new WatchedDir(node, { annotation: 'string node' });
}
}
// For convenience, all nodeWrappers get an `inputNodeWrappers` array; for
// 'source' nodes it's empty.
nodeWrapper.inputNodeWrappers = inputNodeWrappers
nodeWrapper.id = this.nodeWrappers.length
// this.nodeWrappers will contain all the node wrappers in topological
// order, i.e. each node comes after all its input nodes.
//
// It's unfortunate that we're mutating this.nodeWrappers as a side effect,
// but since we work backwards from the output node to discover all the
// input nodes, it's harder to do a side-effect-free topological sort.
this.nodeWrappers.push(nodeWrapper)
return nodeWrapper
}
Builder.prototype.features = broccoliNodeInfo.features
Builder.prototype.checkInputPathsExist = function() {
// We might consider checking this.unwatchedPaths as well.
for (var i = 0; i < this.watchedPaths.length; i++) {
var isDirectory
// Call node.__broccoliGetInfo__()
let nodeInfo;
try {
isDirectory = fs.statSync(this.watchedPaths[i]).isDirectory()
} catch (err) {
throw new Builder.BuilderError('Directory not found: ' + this.watchedPaths[i])
nodeInfo = broccoliNodeInfo.getNodeInfo(node);
} catch (e) {
if (!(e instanceof broccoliNodeInfo.InvalidNodeError)) throw e;
// We don't have the instantiation stack of an invalid node, so to aid
// debugging, we instead report its parent node
const messageSuffix =
_stack.length > 0
? '\nused as input node to ' +
_stack[_stack.length - 1].label +
_stack[_stack.length - 1].formatInstantiationStackForTerminal()
: '\nused as output node';
throw new broccoliNodeInfo.InvalidNodeError(e.message + messageSuffix);
}
if (!isDirectory) {
throw new Builder.BuilderError('Not a directory: ' + this.watchedPaths[i])
}
}
};
Builder.prototype.setupTmpDirs = function() {
// Create temporary directories for each node:
//
// out-01-someplugin/
// out-02-otherplugin/
// cache-01-someplugin/
// cache-02-otherplugin/
//
// Here's an alternative directory structure we might consider (it's not
// clear which structure makes debugging easier):
//
// 01-someplugin/
// out/
// cache/
// in-1 -> ... // symlink for convenience
// in-2 -> ...
// 02-otherplugin/
// ...
var tmpobj = tmp.dirSync({ prefix: 'broccoli-', unsafeCleanup: true, dir: this.tmpdir })
this.builderTmpDir = tmpobj.name
this.builderTmpDirCleanup = tmpobj.removeCallback
for (var i = 0; i < this.nodeWrappers.length; i++) {
var nodeWrapper = this.nodeWrappers[i]
if (nodeWrapper.nodeInfo.nodeType === 'transform') {
nodeWrapper.inputPaths = nodeWrapper.inputNodeWrappers.map(function(nw) {
return nw.outputPath
})
nodeWrapper.outputPath = this.mkTmpDir(nodeWrapper, 'out')
// Compute label, like "Funnel (test suite)"
let label = nodeInfo.name;
const labelExtras = [];
if (nodeInfo.nodeType === 'source') labelExtras.push(nodeInfo.sourceDirectory);
if (nodeInfo.annotation != null) labelExtras.push(nodeInfo.annotation);
if (labelExtras.length > 0) label += ' (' + labelExtras.join('; ') + ')';
if (nodeWrapper.nodeInfo.needsCache) {
nodeWrapper.cachePath = this.mkTmpDir(nodeWrapper, 'cache')
// We start constructing the nodeWrapper here because we'll need the partial
// nodeWrapper for the _stack. Later we'll add more properties.
const nodeWrapper =
nodeInfo.nodeType === 'transform' ? new TransformNodeWrapper() : new SourceNodeWrapper();
nodeWrapper.nodeInfo = nodeInfo;
nodeWrapper.originalNode = originalNode;
nodeWrapper.node = node;
nodeWrapper.label = label;
// Detect cycles
for (let i = 0; i < _stack.length; i++) {
if (_stack[i].node === originalNode) {
let cycleMessage = 'Cycle in node graph: ';
for (let j = i; j < _stack.length; j++) {
cycleMessage += _stack[j].label + ' -> ';
}
cycleMessage += nodeWrapper.label;
throw new this.constructor.BuilderError(cycleMessage);
}
} else { // nodeType === 'source'
// We could name this .sourcePath, but with .outputPath the code is simpler.
nodeWrapper.outputPath = nodeWrapper.nodeInfo.sourceDirectory
}
}
}
// Create temporary directory, like
// /tmp/broccoli-9rLfJh/out-067-merge_trees_vendor_packages
// type is 'out' or 'cache'
Builder.prototype.mkTmpDir = function(nodeWrapper, type) {
var nameAndAnnotation = nodeWrapper.nodeInfo.name + ' ' + (nodeWrapper.nodeInfo.annotation || '')
// slugify turns fooBar into foobar, so we call underscored first to
// preserve word boundaries
var suffix = underscoreString.underscored(nameAndAnnotation.substr(0, 60))
suffix = underscoreString.slugify(suffix).replace(/-/g, '_')
// 1 .. 147 -> '001' .. '147'
var paddedId = underscoreString.pad('' + nodeWrapper.id, ('' + this.nodeWrappers.length).length, '0')
var dirname = type + '-' + paddedId + '-' + suffix
var tmpDir = path.join(this.builderTmpDir, dirname)
fs.mkdirSync(tmpDir)
return tmpDir
}
Builder.prototype.setupNodes = function() {
for (var i = 0; i < this.nodeWrappers.length; i++) {
var nw = this.nodeWrappers[i]
try {
nw.setup(this.features)
} catch (err) {
throw new NodeSetupError(err, nw)
// For 'transform' nodes, recurse into the input nodes; for 'source' nodes,
// record paths.
let inputNodeWrappers = [];
if (nodeInfo.nodeType === 'transform') {
const newStack = _stack.concat([nodeWrapper]);
inputNodeWrappers = nodeInfo.inputNodes.map(inputNode => {
return this.makeNodeWrapper(inputNode, newStack);
});
} else {
// nodeType === 'source'
if (nodeInfo.watched) {
this.watchedPaths.push(nodeInfo.sourceDirectory);
} else {
this.unwatchedPaths.push(nodeInfo.sourceDirectory);
}
}
}
}
// For convenience, all nodeWrappers get an `inputNodeWrappers` array; for
// 'source' nodes it's empty.
nodeWrapper.inputNodeWrappers = inputNodeWrappers;
// Base class for builder errors
Builder.BuilderError = BuilderError
BuilderError.prototype = Object.create(Error.prototype)
BuilderError.prototype.constructor = BuilderError
function BuilderError(message) {
// Subclassing Error in ES5 is non-trivial because reasons, so we need this
// extra constructor logic from http://stackoverflow.com/a/17891099/525872.
// Note that ES5 subclasses of BuilderError don't in turn need any special
// code.
var temp = Error.apply(this, arguments)
// Need to assign temp.name for correct error class in .stack and .message
temp.name = this.name = this.constructor.name
this.stack = temp.stack
this.message = temp.message
}
nodeWrapper.id = this.nodeWrappers.length;
Builder.InvalidNodeError = broccoliNodeInfo.InvalidNodeError
// this.nodeWrappers will contain all the node wrappers in topological
// order, i.e. each node comes after all its input nodes.
//
// It's unfortunate that we're mutating this.nodeWrappers as a side effect,
// but since we work backwards from the output node to discover all the
// input nodes, it's harder to do a side-effect-free topological sort.
this.nodeWrappers.push(nodeWrapper);
Builder.NodeSetupError = NodeSetupError
NodeSetupError.prototype = Object.create(BuilderError.prototype)
NodeSetupError.prototype.constructor = NodeSetupError
function NodeSetupError(originalError, nodeWrapper) {
if (nodeWrapper == null) { // Chai calls new NodeSetupError() :(
BuilderError.call(this)
return
return nodeWrapper;
}
originalError = wrapPrimitiveErrors(originalError)
var message = originalError.message +
'\nat ' + nodeWrapper.label +
nodeWrapper.formatInstantiationStackForTerminal()
BuilderError.call(this, message)
// The stack will have the original exception name, but that's OK
this.stack = originalError.stack
}
Builder.BuildError = BuildError
BuildError.prototype = Object.create(BuilderError.prototype)
BuildError.prototype.constructor = BuildError
function BuildError(originalError, nodeWrapper) {
if (nodeWrapper == null) { // for Chai
BuilderError.call(this)
return
checkInputPathsExist() {
// We might consider checking this.unwatchedPaths as well.
for (let i = 0; i < this.watchedPaths.length; i++) {
let isDirectory;
try {
isDirectory = fs.statSync(this.watchedPaths[i]).isDirectory();
} catch (err) {
throw new this.constructor.BuilderError('Directory not found: ' + this.watchedPaths[i]);
}
if (!isDirectory) {
throw new this.constructor.BuilderError('Not a directory: ' + this.watchedPaths[i]);
}
}
}
originalError = wrapPrimitiveErrors(originalError)
setupTmpDirs() {
// Create temporary directories for each node:
//
// out-01-someplugin/
// out-02-otherplugin/
// cache-01-someplugin/
// cache-02-otherplugin/
//
// Here's an alternative directory structure we might consider (it's not
// clear which structure makes debugging easier):
//
// 01-someplugin/
// out/
// cache/
// in-1 -> ... // symlink for convenience
// in-2 -> ...
// 02-otherplugin/
// ...
const tmpobj = tmp.dirSync({
prefix: 'broccoli-',
unsafeCleanup: true,
dir: this.tmpdir,
});
// Create heavily augmented message for easy printing to the terminal. Web
// interfaces should refer to broccoliPayload.originalError.message instead.
var filePart = ''
if (originalError.file != null) {
filePart = originalError.file
if (originalError.line != null) {
filePart += ':' + originalError.line
if (originalError.column != null) {
// .column is zero-indexed
filePart += ':' + (originalError.column + 1)
this.builderTmpDir = tmpobj.name;
this.builderTmpDirCleanup = tmpobj.removeCallback;
for (let i = 0; i < this.nodeWrappers.length; i++) {
const nodeWrapper = this.nodeWrappers[i];
if (nodeWrapper.nodeInfo.nodeType === 'transform') {
nodeWrapper.inputPaths = nodeWrapper.inputNodeWrappers.map(function(nw) {
return nw.outputPath;
});
nodeWrapper.outputPath = this.mkTmpDir(nodeWrapper, 'out');
if (nodeWrapper.nodeInfo.needsCache) {
nodeWrapper.cachePath = this.mkTmpDir(nodeWrapper, 'cache');
}
} else {
// nodeType === 'source'
// We could name this .sourcePath, but with .outputPath the code is simpler.
nodeWrapper.outputPath = nodeWrapper.nodeInfo.sourceDirectory;
}
}
filePart += ': '
}
var instantiationStack = ''
if (originalError.file == null) {
// We want to report the instantiation stack only for "unexpected" errors
// (bugs, internal errors), but not for compiler errors and such. For now,
// the presence of `.file` serves as a heuristic to distinguish between
// those cases.
instantiationStack = nodeWrapper.formatInstantiationStackForTerminal()
// Create temporary directory, like
// /tmp/broccoli-9rLfJh/out-067-merge_trees_vendor_packages
// type is 'out' or 'cache'
mkTmpDir(nodeWrapper, type) {
let nameAndAnnotation =
nodeWrapper.nodeInfo.name + ' ' + (nodeWrapper.nodeInfo.annotation || '');
// slugify turns fooBar into foobar, so we call underscored first to
// preserve word boundaries
let suffix = underscoreString.underscored(nameAndAnnotation.substr(0, 60));
suffix = underscoreString.slugify(suffix).replace(/-/g, '_');
// 1 .. 147 -> '001' .. '147'
const paddedId = underscoreString.pad(
'' + nodeWrapper.id,
('' + this.nodeWrappers.length).length,
'0'
);
const dirname = type + '-' + paddedId + '-' + suffix;
const tmpDir = path.join(this.builderTmpDir, dirname);
fs.mkdirSync(tmpDir);
return tmpDir;
}
var message = filePart + originalError.message +
(originalError.treeDir ? '\n in ' + originalError.treeDir : '') +
'\n at ' + nodeWrapper.label +
instantiationStack
BuilderError.call(this, message)
this.stack = originalError.stack
// This error API can change between minor Broccoli version bumps
this.broccoliPayload = {
originalError: originalError, // guaranteed to be error object, not primitive
originalMessage: originalError.message,
// node info
nodeId: nodeWrapper.id,
nodeLabel: nodeWrapper.label,
nodeName: nodeWrapper.nodeInfo.name,
nodeAnnotation: nodeWrapper.nodeInfo.annotation,
instantiationStack: nodeWrapper.nodeInfo.instantiationStack,
// error location (if any)
location: {
file: originalError.file,
treeDir: originalError.treeDir,
line: originalError.line,
column: originalError.column
setupNodes() {
for (let i = 0; i < this.nodeWrappers.length; i++) {
const nw = this.nodeWrappers[i];
try {
nw.setup(this.features);
} catch (err) {
throw new NodeSetupError(err, nw);
}
}
}
}
setupHeimdall() {
this.on('beginNode', node => {
let name;
Builder.NodeWrapper = NodeWrapper
function NodeWrapper() {
this.buildState = {}
}
if (node instanceof SourceNodeWrapper) {
name = node.nodeInfo.sourceDirectory;
} else {
name = node.nodeInfo.annotation || node.nodeInfo.name;
}
Builder.TransformNodeWrapper = TransformNodeWrapper
TransformNodeWrapper.prototype = Object.create(NodeWrapper.prototype)
TransformNodeWrapper.prototype.constructor = TransformNodeWrapper
function TransformNodeWrapper() {
NodeWrapper.apply(this, arguments)
}
node.__heimdall_cookie__ = heimdall.start({
name,
label: node.label,
broccoliNode: true,
broccoliId: node.id,
broccoliCachedNode: false,
broccoliPluginName: node.nodeInfo.name,
});
node.__heimdall__ = heimdall.current;
});
Builder.SourceNodeWrapper = SourceNodeWrapper
SourceNodeWrapper.prototype = Object.create(NodeWrapper.prototype)
SourceNodeWrapper.prototype.constructor = SourceNodeWrapper
function SourceNodeWrapper() {
NodeWrapper.apply(this, arguments)
}
this.on('endNode', node => {
if (node.__heimdall__) {
node.__heimdall_cookie__.stop();
}
});
}
TransformNodeWrapper.prototype.setup = function(features) {
this.nodeInfo.setup(features, {
inputPaths: this.inputPaths,
outputPath: this.outputPath,
cachePath: this.cachePath
})
this.callbackObject = this.nodeInfo.getCallbackObject()
}
buildHeimdallTree(outputNodeWrapper) {
const heimdallRootNode = outputNodeWrapper.__heimdall__;
const heimdallByBroccoliId = {};
SourceNodeWrapper.prototype.setup = function(features) {
}
if (!heimdallRootNode) {
return;
}
// Call node.build(), plus bookkeeping
TransformNodeWrapper.prototype.build = function() {
var self = this
heimdallRootNode.parent.forEachChild(child => {
if (!child.id.broccoliNode) {
return;
}
var startTime = process.hrtime()
if (!this.nodeInfo.persistentOutput) {
rimraf.sync(this.outputPath)
fs.mkdirSync(this.outputPath)
}
return RSVP.resolve(self.callbackObject.build())
.then(function() {
var now = process.hrtime()
// Build time in milliseconds
self.buildState.selfTime = 1000 * ((now[0] - startTime[0]) + (now[1] - startTime[1]) / 1e9)
self.buildState.totalTime = self.buildState.selfTime
for (var i = 0; i < self.inputNodeWrappers.length; i++) {
self.buildState.totalTime += self.inputNodeWrappers[i].buildState.totalTime
// Skip the outputNodeWrapper node
if (child === heimdallRootNode) {
return;
}
})
}
SourceNodeWrapper.prototype.build = function() {
// We only check here that the sourceDirectory exists and is a directory
try {
if (!fs.statSync(this.nodeInfo.sourceDirectory).isDirectory()) {
throw new Error('Not a directory')
}
} catch (err) { // stat might throw, or we might throw
err.file = this.nodeInfo.sourceDirectory
// fs.stat augments error message with file name, but that's redundant
// with our err.file, so we strip it
err.message = err.message.replace(/, stat '[^'\n]*'$/m, '')
throw err
}
heimdallByBroccoliId[child.id.broccoliId] = child;
});
this.buildState.selfTime = 0
this.buildState.totalTime = 0
}
const processed = {};
TransformNodeWrapper.prototype.toString = function() {
var hint = this.label
hint = this.label
if (this.inputNodeWrappers) { // a bit defensive to deal with partially-constructed node wrappers
hint += ' inputNodeWrappers:[' + this.inputNodeWrappers.map(function(nw) { return nw.id }) + ']'
}
hint += ' at ' + this.outputPath
if (this.buildState.selfTime != null) {
hint += ' (' + Math.round(this.buildState.selfTime) + ' ms)'
}
return '[NodeWrapper:' + this.id + ' ' + hint + ']'
}
// Traverse the node tree from bottom to top, and add each node to its parents children
const traverseTree = function traverseTree(nodeWrapper, heimdallNode) {
heimdallNode.stats.time.total = heimdallNode.stats.time.self;
SourceNodeWrapper.prototype.toString = function() {
var hint = this.nodeInfo.sourceDirectory +
(this.nodeInfo.watched ? '' : ' (unwatched)')
return '[NodeWrapper:' + this.id + ' ' + hint + ']'
}
// Iterate each inputNodeWrapper and push this nodes onto its children
for (let inputNodeWrapper of nodeWrapper.inputNodeWrappers) {
let childHeimdallNode = heimdallByBroccoliId[inputNodeWrapper.id];
NodeWrapper.prototype.toJSON = function() {
return undefinedToNull({
id: this.id,
nodeInfo: this.nodeInfoToJSON(),
buildState: this.buildState,
label: this.label,
inputNodeWrappers: this.inputNodeWrappers.map(function(nw) { return nw.id }),
cachePath: this.cachePath,
outputPath: this.outputPath
// leave out node, originalNode, inputPaths (redundant), build
})
}
// Heimdall does not allow multiple parents. As such, we must create a new "dummy" node
// for any nodes that are re-used (like source nodes).
if (processed[inputNodeWrapper.id]) {
const cookie = heimdall.start(Object.assign({}, childHeimdallNode.id));
childHeimdallNode = heimdall.current;
childHeimdallNode.id.broccoliCachedNode = true;
cookie.stop();
childHeimdallNode.stats.time.self = 0;
}
TransformNodeWrapper.prototype.nodeInfoToJSON = function() {
return undefinedToNull({
nodeType: 'transform',
name: this.nodeInfo.name,
annotation: this.nodeInfo.annotation,
persistentOutput: this.nodeInfo.persistentOutput,
needsCache: this.nodeInfo.needsCache
// leave out instantiationStack (too long), inputNodes, and callbacks
})
}
// Track that this node has been processed so we can duplicate above
processed[inputNodeWrapper.id] = true;
SourceNodeWrapper.prototype.nodeInfoToJSON = function() {
return undefinedToNull({
nodeType: 'source',
sourceDirectory: this.nodeInfo.sourceDirectory,
watched: this.nodeInfo.watched,
name: this.nodeInfo.name,
annotation: this.nodeInfo.annotation
// leave out instantiationStack
})
}
// Remove the node from its existing parent, and add to this node
childHeimdallNode.remove();
heimdallNode.addChild(childHeimdallNode);
heimdallNode.stats.time.total += traverseTree(inputNodeWrapper, childHeimdallNode);
}
NodeWrapper.prototype.formatInstantiationStackForTerminal = function() {
return '\n-~- created here: -~-\n' + this.nodeInfo.instantiationStack + '\n-~- (end) -~-'
}
return heimdallNode.stats.time.total;
};
const time = traverseTree(outputNodeWrapper, heimdallRootNode);
heimdallRootNode.parent.stats.time.total = heimdallRootNode.parent.stats.time.self + time;
}
// Replace all `undefined` values with `null`, so that they show up in JSON output
function undefinedToNull(obj) {
for (var key in obj) {
if (obj.hasOwnProperty(key) && obj[key] === undefined) {
obj[key] = null
}
get features() {
return broccoliNodeInfo.features;
}
return obj
}
};
function wrapPrimitiveErrors(err) {
if (err !== null && typeof err === 'object') {
return err
} else {
// We could augment the message with " [string exception]" to indicate
// that the stack trace is not useful, or even set the .stack to null.
return new Error(err + '')
}
}
RSVP.EventTarget.mixin(module.exports.prototype);
module.exports.BuilderError = BuilderError;
module.exports.InvalidNodeError = broccoliNodeInfo.InvalidNodeError;
module.exports.NodeSetupError = NodeSetupError;
module.exports.BuildError = BuildError;
module.exports.NodeWrapper = NodeWrapper;
module.exports.TransformNodeWrapper = TransformNodeWrapper;
module.exports.SourceNodeWrapper = SourceNodeWrapper;

@@ -1,65 +0,212 @@

var fs = require('fs')
var program = require('commander')
var copyDereferenceSync = require('copy-dereference').sync
'use strict';
var broccoli = require('./index')
var Watcher = require('./watcher')
const RSVP = require('rsvp');
const TreeSync = require('tree-sync');
const childProcess = require('child_process');
const fs = require('fs');
const WatchDetector = require('watch-detector');
const path = require('path');
const broccoli = require('./index');
const messages = require('./messages');
const CliError = require('./errors/cli');
module.exports = broccoliCLI
function broccoliCLI (args) {
var actionPerformed = false
module.exports = function broccoliCLI(args) {
// always require a fresh commander, as it keeps state at module scope
delete require.cache[require.resolve('commander')];
const program = require('commander');
let actionPromise;
program.version(require('../package.json').version).usage('[options] <command> [<args ...>]');
program
.version(JSON.parse(fs.readFileSync(__dirname + '/../package.json', 'utf8')).version)
.usage('[options] <command> [<args ...>]')
program.command('serve')
.command('serve')
.alias('s')
.description('start a broccoli server')
.option('--port <port>', 'the port to bind to [4200]', 4200)
.option('--host <host>', 'the host to bind to [localhost]', 'localhost')
.action(function(options) {
actionPerformed = true
broccoli.server.serve(new Watcher(getBuilder()), options.host, parseInt(options.port, 10))
})
.option('--brocfile-path <path>', 'the path to brocfile')
.option('--output-path <path>', 'the path to target output folder')
.option('--cwd <path>', 'the path to working folder')
.option('--no-watch', 'turn off the watcher')
.option('--watcher <watcher>', 'select sane watcher mode')
.option('--overwrite', 'overwrite the [target/--output-path] directory')
.action(options => {
const builder = getBuilder(options);
const Watcher = getWatcher(options);
const outputDir = options.outputPath;
const watcher = new Watcher(builder, buildWatcherOptions(options));
program.command('build <target>')
if (outputDir) {
try {
guardOutputDir(outputDir, options.overwrite);
} catch (e) {
if (e instanceof CliError) {
console.error(e.message);
return process.exit(1);
}
throw e;
}
const outputTree = new TreeSync(builder.outputPath, outputDir);
watcher.on('buildSuccess', function() {
outputTree.sync();
});
}
const server = broccoli.server.serve(watcher, options.host, parseInt(options.port, 10));
actionPromise = (server && server.closingPromise) || RSVP.resolve();
});
program
.command('build [target]')
.alias('b')
.description('output files to target directory')
.action(function(outputDir) {
actionPerformed = true
if (fs.existsSync(outputDir)) {
console.error(outputDir + '/ already exists; we cannot build into an existing directory')
process.exit(1)
.option('--brocfile-path <path>', 'the path to brocfile')
.option('--output-path <path>', 'the path to target output folder')
.option('--cwd <path>', 'the path to working folder')
.option('--watch', 'turn on the watcher')
.option('--watcher <watcher>', 'select sane watcher mode')
.option('--overwrite', 'overwrite the [target/--output-path] directory')
.action((outputDir, options) => {
if (outputDir && options.outputPath) {
console.error('option --output-path and [target] cannot be passed at same time');
return process.exit(1);
}
var builder = getBuilder()
builder.build()
.then(function() {
copyDereferenceSync(builder.outputPath, outputDir)
if (options.outputPath) {
outputDir = options.outputPath;
}
if (!outputDir) {
outputDir = 'dist';
}
try {
guardOutputDir(outputDir, options.overwrite);
} catch (e) {
if (e instanceof CliError) {
console.error(e.message);
return process.exit(1);
}
throw e;
}
const builder = getBuilder(options);
const Watcher = getWatcher(options);
const outputTree = new TreeSync(builder.outputPath, outputDir);
const watcher = new Watcher(builder, buildWatcherOptions(options));
watcher.on('buildSuccess', () => {
outputTree.sync();
messages.onBuildSuccess(builder);
if (!options.watch) {
watcher.quit();
}
});
watcher.on('buildFailure', messages.onBuildFailure);
function cleanupAndExit() {
return watcher.quit();
}
process.on('SIGINT', cleanupAndExit);
process.on('SIGTERM', cleanupAndExit);
actionPromise = watcher
.start()
.catch(err => console.log((err && err.stack) || err))
.finally(() => {
builder.cleanup();
process.exit(0);
})
.finally(function () {
return builder.cleanup()
})
.then(function () {
process.exit(0)
})
.catch(function (err) {
// Should show file and line/col if present
if (err.file) {
console.error('File: ' + err.file)
}
console.error(err.stack)
console.error('\nBuild failed')
process.exit(1)
})
})
.catch(err => {
console.log('Cleanup error:');
console.log((err && err.stack) || err);
process.exit(1);
});
});
program.parse(args || process.argv)
if(!actionPerformed) {
program.outputHelp()
process.exit(1)
program.parse(args || process.argv);
if (!actionPromise) {
program.outputHelp();
return process.exit(1);
}
return actionPromise || RSVP.resolve();
};
function getBuilder(options) {
const brocfile = broccoli.loadBrocfile(options);
return new broccoli.Builder(brocfile);
}
function getBuilder () {
var node = broccoli.loadBrocfile()
return new broccoli.Builder(node)
function getWatcher(options) {
return options.watch ? broccoli.Watcher : require('./dummy-watcher');
}
function buildWatcherOptions(options) {
if (!options) {
options = {};
}
const detector = new WatchDetector({
ui: { writeLine: console.log },
childProcess,
fs,
watchmanSupportsPlatform: /^win/.test(process.platform),
root: process.cwd(),
});
const watchPreference = detector.findBestWatcherOption({
watcher: options.watcher,
});
const watcher = watchPreference.watcher;
return {
saneOptions: {
poll: watcher === 'polling',
watchman: watcher === 'watchman',
node: watcher === 'node' || !watcher,
},
};
}
function guardOutputDir(outputDir, overwrite) {
if (!fs.existsSync(outputDir)) {
return;
}
if (!overwrite) {
throw new CliError(
outputDir +
'/ already exists; we cannot build into an existing directory, ' +
'pass --overwrite to overwrite the output directory'
);
}
if (isParentDirectory(outputDir)) {
throw new CliError(
'option --overwrite can not be used if outputPath is a parent directory: ' + outputDir
);
}
}
function isParentDirectory(outputPath) {
outputPath = fs.realpathSync(outputPath);
const rootPath = process.cwd();
const rootPathParents = [rootPath];
let dir = path.dirname(rootPath);
rootPathParents.push(dir);
while (dir !== path.dirname(dir)) {
dir = path.dirname(dir);
rootPathParents.push(dir);
}
return rootPathParents.indexOf(outputPath) !== -1;
}

@@ -1,7 +0,25 @@

exports.Builder = require('./builder')
exports.loadBrocfile = require('./load_brocfile')
exports.server = require('./server')
exports.getMiddleware = require('./middleware')
exports.Watcher = require('./watcher')
exports.WatcherAdapter = require('./watcher_adapter')
exports.cli = require('./cli')
'use strict';
module.exports = {
get Builder() {
return require('./builder');
},
get loadBrocfile() {
return require('./load_brocfile');
},
get server() {
return require('./server');
},
get getMiddleware() {
return require('./middleware');
},
get Watcher() {
return require('./watcher');
},
get WatcherAdapter() {
return require('./watcher_adapter');
},
get cli() {
return require('./cli');
},
};

@@ -1,21 +0,32 @@

var path = require('path')
var findup = require('findup-sync')
'use strict';
module.exports = loadBrocfile
function loadBrocfile () {
var brocfile = findup('Brocfile.js', {
nocase: true
})
const path = require('path');
const findup = require('findup-sync');
if (brocfile == null) throw new Error('Brocfile.js not found')
module.exports = function loadBrocfile(options) {
if (!options) {
options = {};
}
var baseDir = path.dirname(brocfile)
let brocfile;
if (options.brocfilePath) {
brocfile = path.resolve(options.brocfilePath);
} else {
brocfile = findup('Brocfile.js', {
nocase: true,
});
}
if (!brocfile) {
throw new Error('Brocfile.js not found');
}
const baseDir = options.cwd || path.dirname(brocfile);
// The chdir should perhaps live somewhere else and not be a side effect of
// this function, or go away entirely
process.chdir(baseDir)
process.chdir(baseDir);
var node = require(brocfile)
return node
}
return require(brocfile);
};

@@ -1,11 +0,17 @@

var path = require('path')
var fs = require('fs')
'use strict';
var handlebars = require('handlebars')
var url = require('url')
var mime = require('mime')
const path = require('path');
const fs = require('fs');
var errorTemplate = handlebars.compile(fs.readFileSync(path.resolve(__dirname, 'templates/error.html')).toString())
var dirTemplate = handlebars.compile(fs.readFileSync(path.resolve(__dirname, 'templates/dir.html')).toString())
const handlebars = require('handlebars');
const url = require('url');
const mime = require('mime');
const errorTemplate = handlebars.compile(
fs.readFileSync(path.resolve(__dirname, 'templates/error.html')).toString()
);
const dirTemplate = handlebars.compile(
fs.readFileSync(path.resolve(__dirname, 'templates/dir.html')).toString()
);
// You must call watcher.start() before you call `getMiddleware`

@@ -20,110 +26,124 @@ //

module.exports = function getMiddleware(watcher, options) {
if (options == null) options = {}
if (options.autoIndex == null) options.autoIndex = true
if (options == null) options = {};
if (options.autoIndex == null) options.autoIndex = true;
var outputPath = watcher.builder.outputPath
const outputPath = watcher.builder.outputPath;
return function broccoliMiddleware(request, response, next) {
if (watcher.currentBuild == null) {
throw new Error('Waiting for initial build to start')
throw new Error('Waiting for initial build to start');
}
watcher.currentBuild.then(function() {
var urlObj = url.parse(request.url)
var filename = path.join(outputPath, decodeURIComponent(urlObj.pathname))
var stat, lastModified, type, charset, buffer
watcher.currentBuild
.then(
() => {
const urlObj = url.parse(request.url);
let filename = path.join(outputPath, decodeURIComponent(urlObj.pathname));
let stat, lastModified, type, charset, buffer;
// contains null byte or escapes directory
if (filename.indexOf('\0') !== -1 || filename.indexOf(outputPath) !== 0) {
response.writeHead(400)
response.end()
return
}
// contains null byte or escapes directory
if (filename.indexOf('\0') !== -1 || filename.indexOf(outputPath) !== 0) {
response.writeHead(400);
response.end();
return;
}
try {
stat = fs.statSync(filename)
} catch (e) {
// not found
next()
return
}
try {
stat = fs.statSync(filename);
} catch (e) {
// not found
next();
return;
}
if (stat.isDirectory()) {
var hasIndex = fs.existsSync(path.join(filename, 'index.html'))
if (stat.isDirectory()) {
const hasIndex = fs.existsSync(path.join(filename, 'index.html'));
if (!hasIndex && !options.autoIndex) {
next()
return
}
if (!hasIndex && !options.autoIndex) {
next();
return;
}
// If no trailing slash, redirect. We use path.sep because filename
// has backslashes on Windows.
if (filename[filename.length - 1] !== path.sep) {
urlObj.pathname += '/'
response.setHeader('Location', url.format(urlObj))
response.setHeader('Cache-Control', 'private, max-age=0, must-revalidate')
response.writeHead(301)
response.end()
return
}
// If no trailing slash, redirect. We use path.sep because filename
// has backslashes on Windows.
if (filename[filename.length - 1] !== path.sep) {
urlObj.pathname += '/';
response.setHeader('Location', url.format(urlObj));
response.setHeader('Cache-Control', 'private, max-age=0, must-revalidate');
response.writeHead(301);
response.end();
return;
}
if (!hasIndex) { // implied: options.autoIndex is true
var context = {
url: request.url,
files: fs.readdirSync(filename).sort().map(function (child){
var stat = fs.statSync(path.join(filename,child)),
isDir = stat.isDirectory()
return {
href: child + (isDir ? '/' : ''),
type: isDir ? 'dir' : path.extname(child).replace('.', '').toLowerCase()
}
}),
liveReloadPath: options.liveReloadPath
if (!hasIndex) {
// implied: options.autoIndex is true
const context = {
url: request.url,
files: fs
.readdirSync(filename)
.sort()
.map(child => {
const stat = fs.statSync(path.join(filename, child)),
isDir = stat.isDirectory();
return {
href: child + (isDir ? '/' : ''),
type: isDir
? 'dir'
: path
.extname(child)
.replace('.', '')
.toLowerCase(),
};
}),
liveReloadPath: options.liveReloadPath,
};
response.setHeader('Cache-Control', 'private, max-age=0, must-revalidate');
response.writeHead(200);
response.end(dirTemplate(context));
return;
}
// otherwise serve index.html
filename += 'index.html';
stat = fs.statSync(filename);
}
response.setHeader('Cache-Control', 'private, max-age=0, must-revalidate')
response.writeHead(200)
response.end(dirTemplate(context))
return
}
// otherwise serve index.html
filename += 'index.html'
stat = fs.statSync(filename)
}
lastModified = stat.mtime.toUTCString();
response.setHeader('Last-Modified', lastModified);
// nginx style treat last-modified as a tag since browsers echo it back
if (request.headers['if-modified-since'] === lastModified) {
response.writeHead(304);
response.end();
return;
}
lastModified = stat.mtime.toUTCString()
response.setHeader('Last-Modified', lastModified)
// nginx style treat last-modified as a tag since browsers echo it back
if (request.headers['if-modified-since'] === lastModified) {
response.writeHead(304)
response.end()
return
}
type = mime.lookup(filename);
charset = mime.charsets.lookup(type);
if (charset) {
type += '; charset=' + charset;
}
response.setHeader('Cache-Control', 'private, max-age=0, must-revalidate');
response.setHeader('Content-Length', stat.size);
response.setHeader('Content-Type', type);
type = mime.lookup(filename)
charset = mime.charsets.lookup(type)
if (charset) {
type += '; charset=' + charset
}
response.setHeader('Cache-Control', 'private, max-age=0, must-revalidate')
response.setHeader('Content-Length', stat.size)
response.setHeader('Content-Type', type)
// read file sync so we don't hold open the file creating a race with
// the builder (Windows does not allow us to delete while the file is open).
buffer = fs.readFileSync(filename)
response.writeHead(200)
response.end(buffer)
}, function(buildError) {
// All errors thrown from builder.build() are guaranteed to be
// Builder.BuildError instances.
var context = {
stack: buildError.stack,
liveReloadPath: options.liveReloadPath,
payload: buildError.broccoliPayload
}
response.setHeader('Content-Type', 'text/html')
response.writeHead(500)
response.end(errorTemplate(context))
}).catch(function(err) { console.log(err.stack) })
}
}
// read file sync so we don't hold open the file creating a race with
// the builder (Windows does not allow us to delete while the file is open).
buffer = fs.readFileSync(filename);
response.writeHead(200);
response.end(buffer);
},
buildError => {
// All errors thrown from builder.build() are guaranteed to be
// Builder.BuildError instances.
const context = {
stack: buildError.stack,
liveReloadPath: options.liveReloadPath,
payload: buildError.broccoliPayload,
};
response.setHeader('Content-Type', 'text/html');
response.writeHead(500);
response.end(errorTemplate(context));
}
)
.catch(err => err.stack);
};
};

@@ -1,68 +0,57 @@

var middleware = require('./middleware')
var http = require('http')
var connect = require('connect')
var printSlowNodes = require('broccoli-slow-trees')
'use strict';
exports.serve = serve
function serve (watcher, host, port) {
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)')
const http = require('http');
var server = {
onBuildSuccessful: function () {
printSlowNodes(server.builder.outputNodeWrapper)
console.log('Built - ' + Math.round(server.builder.outputNodeWrapper.buildState.totalTime) + ' ms @ ' + new Date().toString())
}
}
const messages = require('./messages');
const middleware = require('./middleware');
console.log('Serving on http://' + host + ':' + port + '\n')
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)');
server.watcher = watcher
server.builder = server.watcher.builder
let connect = arguments.length > 3 ? _connect : require('connect');
server.app = connect().use(middleware(server.watcher))
const server = {
onBuildSuccessful: () => messages.onBuildSuccess(watcher.builder),
cleanupAndExit,
};
server.http = http.createServer(server.app)
console.log('Serving on http://' + host + ':' + port + '\n');
server.watcher = watcher;
server.builder = server.watcher.builder;
server.app = connect().use(middleware(server.watcher));
server.http = http.createServer(server.app);
// We register these so the 'exit' handler removing temp dirs is called
function cleanupAndExit() {
return server.watcher.quit()
return server.watcher.quit();
}
process.on('SIGINT', cleanupAndExit)
process.on('SIGTERM', cleanupAndExit)
process.on('SIGINT', cleanupAndExit);
process.on('SIGTERM', cleanupAndExit);
server.watcher.on('buildSuccess', function () {
server.onBuildSuccessful()
})
server.watcher.on('buildSuccess', () => server.onBuildSuccessful());
server.watcher.on('buildFailure', messages.onBuildFailure);
server.watcher.on('buildFailure', function(err) {
console.log('Built with error:')
console.log(err.message)
if (!err.broccoliPayload || !err.broccoliPayload.location.file) {
console.log('')
console.log(err.stack)
}
console.log('')
})
server.watcher.start()
.catch(function(err) {
console.log(err && err.stack || err)
server.closingPromise = server.watcher
.start()
.catch(err => console.log((err && err.stack) || err))
.finally(() => {
server.builder.cleanup();
server.http.close();
process.exit(0);
})
.finally(function() {
server.builder.cleanup()
server.http.close()
})
.catch(function(err) {
console.log('Cleanup error:')
console.log(err && err.stack || err)
})
.finally(function() {
process.exit(1)
})
.catch(err => {
console.log('Cleanup error:');
console.log((err && err.stack) || err);
process.exit(1);
});
server.http.listen(parseInt(port, 10), host)
return server
}
server.http.listen(parseInt(port, 10), host);
return server;
};

@@ -1,53 +0,70 @@

var sane = require('sane')
var RSVP = require('rsvp')
var logger = require('heimdalljs-logger')('broccoli:watcherAdapter')
'use strict';
const sane = require('sane');
const RSVP = require('rsvp');
const logger = require('heimdalljs-logger')('broccoli:watcherAdapter');
function defaultFilterFunction(name) {
return /^[^\.]/.test(name)
return /^[^.]/.test(name);
}
module.exports = WatcherAdapter
RSVP.EventTarget.mixin(WatcherAdapter.prototype)
function WatcherAdapter(options) {
this.options = options || {}
this.options.filter = this.options.filter || defaultFilterFunction
function bindFileEvent(adapter, watcher, event) {
watcher.on(event, (filepath, root) => {
logger.debug(event, root + '/' + filepath);
adapter.trigger('change');
});
}
WatcherAdapter.prototype.watch = function(watchedPaths) {
var self = this
class WatcherAdapter {
constructor(options) {
this.options = options || {};
this.options.filter = this.options.filter || defaultFilterFunction;
this.watchers = [];
}
this.watchers = []
this.readyPromises = []
watchedPaths.forEach(function(watchedPath) {
var watcher = new sane(watchedPath, self.options)
function bindFileEvent(event) {
watcher.on(event, function(filepath, root, stat) {
logger.debug(event, root + '/' + filepath)
self.trigger('change')
})
watch(watchedPaths) {
if (!Array.isArray(watchedPaths)) {
throw new TypeError(`WatcherAdapter#watch's first argument must be an array of watchedPaths`);
}
bindFileEvent('change')
bindFileEvent('add')
bindFileEvent('delete')
watcher.on('error', function(err) {
logger.debug('error', err)
self.trigger('error', err)
})
var readyPromise = new RSVP.Promise(function(resolve, reject) {
watcher.on('ready', function() {
logger.debug('ready', watchedPath)
resolve()
})
})
self.watchers.push(watcher)
self.readyPromises.push(readyPromise)
})
return RSVP.Promise.all(this.readyPromises)
}
WatcherAdapter.prototype.quit = function () {
for (var i = 0; i < this.watchers.length; i++) {
this.watchers[i].close()
let watchers = watchedPaths.map(watchedPath => {
return new Promise(resolve => {
const watcher = new sane(watchedPath, this.options);
this.watchers.push(watcher);
bindFileEvent(this, watcher, 'change');
bindFileEvent(this, watcher, 'add');
bindFileEvent(this, watcher, 'delete');
watcher.on('error', err => {
logger.debug('error', err);
this.trigger('error', err);
});
watcher.on('ready', () => {
logger.debug('ready', watchedPath);
resolve(watcher);
});
});
});
return Promise.all(watchers).then(function() {});
}
quit() {
let closing = this.watchers.map(watcher => {
return new Promise((resolve, reject) => {
watcher.close(err => {
if (err) reject(err);
else resolve();
});
});
});
this.watchers.length = 0;
return Promise.all(closing).then(function() {});
}
}
module.exports = WatcherAdapter;
RSVP.EventTarget.mixin(WatcherAdapter.prototype);
module.exports.bindFileEvent = bindFileEvent;

@@ -1,6 +0,6 @@

'use strict'
'use strict';
var RSVP = require('rsvp')
var WatcherAdapter = require('./watcher_adapter')
var logger = require('heimdalljs-logger')('broccoli:watcher')
const RSVP = require('rsvp');
const WatcherAdapter = require('./watcher_adapter');
const logger = require('heimdalljs-logger')('broccoli:watcher');

@@ -11,125 +11,119 @@ // This Watcher handles all the Broccoli logic, such as debouncing. The

module.exports = Watcher
function Watcher(builder, options) {
this.options = options || {}
if (this.options.debounce == null) this.options.debounce = 100
this.builder = builder
this.watcherAdapter = new WatcherAdapter(this.options.saneOptions)
this.currentBuild = null
this._rebuildScheduled = false
this._ready = false
this._quitting = false
this._lifetimeDeferred = null
}
class Watcher {
constructor(builder, options) {
this.options = options || {};
if (this.options.debounce == null) this.options.debounce = 100;
this.builder = builder;
this.watcherAdapter = new WatcherAdapter(this.options.saneOptions);
this.currentBuild = null;
this._rebuildScheduled = false;
this._ready = false;
this._quitting = false;
this._lifetimeDeferred = null;
}
RSVP.EventTarget.mixin(Watcher.prototype)
start() {
if (this._lifetimeDeferred != null)
throw new Error('Watcher.prototype.start() must not be called more than once');
this._lifetimeDeferred = RSVP.defer();
Watcher.prototype.start = function() {
var self = this
this.watcherAdapter.on('change', this._change.bind(this));
this.watcherAdapter.on('error', this._error.bind(this));
RSVP.resolve()
.then(() => {
return this.watcherAdapter.watch(this.builder.watchedPaths);
})
.then(() => {
logger.debug('ready');
this._ready = true;
this.currentBuild = this._build();
})
.catch(err => this._error(err));
if (this._lifetimeDeferred != null) throw new Error('Watcher.prototype.start() must not be called more than once')
this._lifetimeDeferred = RSVP.defer()
return this._lifetimeDeferred.promise;
}
this.watcherAdapter.on('change', this._change.bind(this))
this.watcherAdapter.on('error', this._error.bind(this))
RSVP.resolve().then(function() {
return self.watcherAdapter.watch(self.builder.watchedPaths)
}).then(function() {
logger.debug('ready')
self._ready = true
self.currentBuild = self._build()
}).catch(function(err) {
self._error(err)
})
_change() {
if (!this._ready) {
logger.debug('change', 'ignored: before ready');
return;
}
if (this._rebuildScheduled) {
logger.debug('change', 'ignored: rebuild scheduled already');
return;
}
logger.debug('change');
this._rebuildScheduled = true;
// Wait for current build, and ignore build failure
RSVP.resolve(this.currentBuild)
.catch(() => {})
.then(() => {
if (this._quitting) return;
const buildPromise = new RSVP.Promise(resolve => {
logger.debug('debounce');
this.trigger('debounce');
setTimeout(resolve, this.options.debounce);
}).then(() => {
// Only set _rebuildScheduled to false *after* the setTimeout so that
// change events during the setTimeout don't trigger a second rebuild
this._rebuildScheduled = false;
return this._build();
});
this.currentBuild = buildPromise;
});
}
return this._lifetimeDeferred.promise
}
_build() {
logger.debug('buildStart');
this.trigger('buildStart');
const buildPromise = this.builder.build();
// Trigger change/error events. Importantly, if somebody else chains to
// currentBuild, their callback will come after our events have
// triggered, because we registered our callback first.
buildPromise.then(
() => {
logger.debug('buildSuccess');
this.trigger('buildSuccess');
},
err => {
logger.debug('buildFailure');
this.trigger('buildFailure', err);
}
);
return buildPromise;
}
Watcher.prototype._change = function() {
var self = this
_error(err) {
logger.debug('error', err);
if (this._quitting) return;
this._quit()
.catch(() => {})
.then(() => this._lifetimeDeferred.reject(err));
}
if (!this._ready) {
logger.debug('change', 'ignored: before ready')
return
quit() {
if (this._quitting) {
logger.debug('quit', 'ignored: already quitting');
return;
}
this._quit().then(
() => this._lifetimeDeferred.resolve(),
err => this._lifetimeDeferred.reject(err)
);
}
if (this._rebuildScheduled) {
logger.debug('change', 'ignored: rebuild scheduled already')
return
}
logger.debug('change')
this._rebuildScheduled = true
// Wait for current build, and ignore build failure
RSVP.resolve(this.currentBuild).catch(function() { }).then(function() {
if (self._quitting) return
var buildPromise = new RSVP.Promise(function(resolve, reject) {
logger.debug('debounce')
self.trigger('debounce')
setTimeout(resolve, self.options.debounce)
}).then(function() {
// Only set _rebuildScheduled to false *after* the setTimeout so that
// change events during the setTimeout don't trigger a second rebuild
self._rebuildScheduled = false
return self._build()
})
self.currentBuild = buildPromise
})
}
Watcher.prototype._build = function() {
var self = this
_quit() {
this._quitting = true;
logger.debug('quitStart');
logger.debug('buildStart')
this.trigger('buildStart')
var buildPromise = self.builder.build()
// Trigger change/error events. Importantly, if somebody else chains to
// currentBuild, their callback will come after our events have
// triggered, because we registered our callback first.
buildPromise.then(function() {
logger.debug('buildSuccess')
self.trigger('buildSuccess')
}, function(err) {
logger.debug('buildFailure')
self.trigger('buildFailure', err)
})
return buildPromise
}
Watcher.prototype._error = function(err) {
var self = this
logger.debug('error', err)
if (this._quitting) return
this._quit().catch(function() { }).then(function() {
self._lifetimeDeferred.reject(err)
})
}
Watcher.prototype.quit = function() {
var self = this
if (this._quitting) {
logger.debug('quit', 'ignored: already quitting')
return
return RSVP.resolve()
.then(() => this.watcherAdapter.quit())
.finally(() => {
// Wait for current build, and ignore build failure
return RSVP.resolve(this.currentBuild).catch(() => {});
})
.finally(() => logger.debug('quitEnd'));
}
this._quit().then(function() {
self._lifetimeDeferred.resolve()
}, function(err) {
self._lifetimeDeferred.reject(err)
})
}
Watcher.prototype._quit = function(err) {
var self = this
this._quitting = true
logger.debug('quitStart')
return RSVP.resolve().then(function() {
return self.watcherAdapter.quit()
}).finally(function() {
// Wait for current build, and ignore build failure
return RSVP.resolve(self.currentBuild).catch(function() { })
}).finally(function() {
logger.debug('quitEnd')
})
}
RSVP.EventTarget.mixin(Watcher.prototype);
module.exports = Watcher;
{
"name": "broccoli",
"description": "Fast client-side asset builder",
"version": "1.1.4",
"version": "2.0.0-beta.1",
"author": "Jo Liss <joliss42@gmail.com>",

@@ -23,36 +23,47 @@ "main": "lib/index.js",

"broccoli-node-info": "1.1.0",
"broccoli-slow-trees": "2.0.0",
"broccoli-slow-trees": "^2.0.0",
"broccoli-source": "^1.1.0",
"commander": "^2.5.0",
"connect": "^3.3.3",
"copy-dereference": "^1.0.0",
"findup-sync": "^1.0.0",
"handlebars": "^4.0.4",
"heimdalljs-logger": "^0.1.7",
"mime": "^1.2.11",
"rimraf": "^2.4.3",
"rsvp": "^3.5.0",
"sane": "^1.4.1",
"tmp": "0.0.31",
"underscore.string": "^3.2.2"
"commander": "^2.11.0",
"connect": "^3.6.5",
"findup-sync": "^2.0.0",
"handlebars": "^4.0.11",
"heimdalljs": "^0.2.3",
"heimdalljs-logger": "^0.1.9",
"mime": "^1.5.0",
"rimraf": "^2.6.2",
"rsvp": "^4.7.0",
"sane": "^2.2.0",
"tmp": "0.0.33",
"tree-sync": "^1.2.2",
"underscore.string": "^3.2.2",
"watch-detector": "^0.1.0"
},
"devDependencies": {
"chai": "^3.3.0",
"chai-as-promised": "^5.1.0",
"fixturify": "^0.2.0",
"mocha": "^3.0.0",
"mocha-jshint": "^2.2.5",
"multidep": "^2.0.0",
"semver": "^5.3.0",
"sinon": "^1.17.1",
"sinon-chai": "^2.8.0",
"symlink-or-copy": "^1.0.1"
"chai": "^4.1.2",
"chai-as-promised": "^7.1.1",
"eslint-config-prettier": "^2.8.0",
"eslint-plugin-mocha": "^4.11.0",
"eslint-plugin-node": "^5.2.1",
"eslint-plugin-prettier": "^2.3.1",
"fixturify": "^0.3.4",
"mocha": "^4.0.1",
"mocha-eslint": "^4.1.0",
"multidep": "^2.0.2",
"portfinder": "^1.0.13",
"prettier": "^1.8.2",
"semver": "^5.4.1",
"sinon": "^4.1.2",
"sinon-chai": "^2.14.0",
"symlink-or-copy": "^1.1.8"
},
"engines": {
"node": ">= 0.10.0"
"node": ">= 4"
},
"scripts": {
"lint": "eslint lib test",
"lint:fix": "eslint --fix lib test",
"pretest": "multidep test/multidep.json",
"test": "mocha"
"test": "mocha",
"test:debug": "mocha --inspect-brk"
}
}
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc