Comparing version 1.1.4 to 2.0.0-beta.1
# 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 @@ |
@@ -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; |
245
lib/cli.js
@@ -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" | ||
} | ||
} |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
29
1332
71454
17
16
1
6
3
+ Addedheimdalljs@^0.2.3
+ Addedtree-sync@^1.2.2
+ Addedwatch-detector@^0.1.0
+ Addedanymatch@2.0.0(transitive)
+ Addedarr-diff@4.0.0(transitive)
+ Addedarr-union@3.1.0(transitive)
+ Addedarray-unique@0.3.2(transitive)
+ Addedassign-symbols@1.0.0(transitive)
+ Addedatob@2.1.2(transitive)
+ Addedbase@0.11.2(transitive)
+ Addedbindings@1.5.0(transitive)
+ Addedbraces@2.3.2(transitive)
+ Addedcache-base@1.0.1(transitive)
+ Addedcapture-exit@1.2.0(transitive)
+ Addedclass-utils@0.3.6(transitive)
+ Addedcollection-visit@1.0.0(transitive)
+ Addedcomponent-emitter@1.3.1(transitive)
+ Addedcopy-descriptor@0.1.1(transitive)
+ Addeddecode-uri-component@0.2.2(transitive)
+ Addeddefine-property@0.2.51.0.02.0.2(transitive)
+ Addeddetect-file@1.0.0(transitive)
+ Addedensure-posix-path@1.1.1(transitive)
+ Addedexpand-brackets@2.1.4(transitive)
+ Addedexpand-tilde@2.0.2(transitive)
+ Addedextend-shallow@2.0.13.0.2(transitive)
+ Addedextglob@2.0.4(transitive)
+ Addedfile-uri-to-path@1.0.0(transitive)
+ Addedfill-range@4.0.0(transitive)
+ Addedfindup-sync@2.0.0(transitive)
+ Addedfragment-cache@0.2.1(transitive)
+ Addedfs-tree-diff@0.5.9(transitive)
+ Addedfsevents@1.2.13(transitive)
+ Addedfunction-bind@1.1.2(transitive)
+ Addedget-value@2.0.6(transitive)
+ Addedglobal-modules@1.0.0(transitive)
+ Addedglobal-prefix@1.0.2(transitive)
+ Addedhas-value@0.3.11.0.0(transitive)
+ Addedhas-values@0.1.41.0.0(transitive)
+ Addedhasown@2.0.2(transitive)
+ Addedis-accessor-descriptor@1.0.1(transitive)
+ Addedis-data-descriptor@1.0.1(transitive)
+ Addedis-descriptor@0.1.71.0.3(transitive)
+ Addedis-extendable@1.0.1(transitive)
+ Addedis-extglob@2.1.1(transitive)
+ Addedis-glob@3.1.0(transitive)
+ Addedis-number@3.0.0(transitive)
+ Addedis-plain-object@2.0.4(transitive)
+ Addedis-windows@1.0.2(transitive)
+ Addedisobject@3.0.1(transitive)
+ Addedkind-of@4.0.0(transitive)
+ Addedmap-cache@0.2.2(transitive)
+ Addedmap-visit@1.0.0(transitive)
+ Addedmatcher-collection@1.1.2(transitive)
+ Addedmicromatch@3.1.10(transitive)
+ Addedmixin-deep@1.3.2(transitive)
+ Addedmkdirp@0.5.6(transitive)
+ Addedmktemp@0.4.0(transitive)
+ Addednan@2.22.0(transitive)
+ Addednanomatch@1.2.13(transitive)
+ Addedobject-assign@4.1.1(transitive)
+ Addedobject-copy@0.1.0(transitive)
+ Addedobject-visit@1.0.1(transitive)
+ Addedobject.pick@1.3.0(transitive)
+ Addedpascalcase@0.1.1(transitive)
+ Addedpath-posix@1.0.0(transitive)
+ Addedposix-character-classes@0.1.1(transitive)
+ Addedquick-temp@0.1.8(transitive)
+ Addedregex-not@1.0.2(transitive)
+ Addedresolve-dir@1.0.1(transitive)
+ Addedresolve-url@0.2.1(transitive)
+ Addedret@0.1.15(transitive)
+ Addedrsvp@4.8.5(transitive)
+ Addedsafe-regex@1.1.0(transitive)
+ Addedsane@2.5.2(transitive)
+ Addedsemver@5.7.2(transitive)
+ Addedset-value@2.0.1(transitive)
+ Addedsilent-error@1.1.1(transitive)
+ Addedsnapdragon@0.8.2(transitive)
+ Addedsnapdragon-node@2.1.1(transitive)
+ Addedsnapdragon-util@3.0.1(transitive)
+ Addedsource-map@0.5.7(transitive)
+ Addedsource-map-resolve@0.5.3(transitive)
+ Addedsource-map-url@0.4.1(transitive)
+ Addedsplit-string@3.1.0(transitive)
+ Addedstatic-extend@0.1.2(transitive)
+ Addedsymlink-or-copy@1.3.1(transitive)
+ Addedtmp@0.0.33(transitive)
+ Addedto-object-path@0.3.0(transitive)
+ Addedto-regex@3.0.2(transitive)
+ Addedto-regex-range@2.1.1(transitive)
+ Addedtree-sync@1.4.0(transitive)
+ Addedunion-value@1.0.1(transitive)
+ Addedunset-value@1.0.0(transitive)
+ Addedurix@0.1.0(transitive)
+ Addeduse@3.1.1(transitive)
+ Addedwalk-sync@0.3.4(transitive)
+ Addedwatch@0.18.0(transitive)
+ Addedwatch-detector@0.1.0(transitive)
- Removedcopy-dereference@^1.0.0
- Removedanymatch@1.3.2(transitive)
- Removedarr-diff@2.0.0(transitive)
- Removedarray-unique@0.2.1(transitive)
- Removedbraces@1.8.5(transitive)
- Removedcopy-dereference@1.0.0(transitive)
- Removeddetect-file@0.1.0(transitive)
- Removedexpand-brackets@0.1.5(transitive)
- Removedexpand-range@1.8.2(transitive)
- Removedexpand-tilde@1.2.2(transitive)
- Removedextglob@0.3.2(transitive)
- Removedfilename-regex@2.0.1(transitive)
- Removedfill-range@2.2.4(transitive)
- Removedfindup-sync@1.0.0(transitive)
- Removedfor-own@0.1.5(transitive)
- Removedfs-exists-sync@0.1.0(transitive)
- Removedglob-base@0.3.0(transitive)
- Removedglob-parent@2.0.0(transitive)
- Removedglobal-modules@0.2.3(transitive)
- Removedglobal-prefix@0.1.5(transitive)
- Removedis-dotfile@1.0.3(transitive)
- Removedis-equal-shallow@0.1.3(transitive)
- Removedis-extglob@1.0.0(transitive)
- Removedis-glob@2.0.1(transitive)
- Removedis-number@2.1.04.0.0(transitive)
- Removedis-posix-bracket@0.1.1(transitive)
- Removedis-primitive@2.0.0(transitive)
- Removedis-windows@0.2.0(transitive)
- Removedmath-random@1.0.4(transitive)
- Removedmicromatch@2.3.11(transitive)
- Removedobject.omit@2.0.1(transitive)
- Removedos-homedir@1.0.2(transitive)
- Removedparse-glob@3.0.4(transitive)
- Removedpreserve@0.2.0(transitive)
- Removedrandomatic@3.1.1(transitive)
- Removedregex-cache@0.4.4(transitive)
- Removedresolve-dir@0.1.1(transitive)
- Removedsane@1.7.0(transitive)
- Removedtmp@0.0.31(transitive)
- Removedwatch@0.10.0(transitive)
Updatedbroccoli-slow-trees@^2.0.0
Updatedcommander@^2.11.0
Updatedconnect@^3.6.5
Updatedfindup-sync@^2.0.0
Updatedhandlebars@^4.0.11
Updatedheimdalljs-logger@^0.1.9
Updatedmime@^1.5.0
Updatedrimraf@^2.6.2
Updatedrsvp@^4.7.0
Updatedsane@^2.2.0
Updatedtmp@0.0.33