Repository: ember-cli/ember-cli
Pull request #6457 (2016-11-23 17:31:48)

Add a sourcemaps doc
Note that this doc is written under the assumption that https://github.com/ef4/fast-sourcemap-concat/pull/30 has landed.  By the time you read this, perhaps it has.  But at the time of writing, it has not.


cc @stefanpenner


- [ ] land https://github.com/ef4/fast-sourcemap-concat/pull/30

Turbo87: regarding the Babel situation: is that a constraint of Babel v5 or does v6 have the same issues?

Patch:
diff --git a/SOURCEMAPS.md b/SOURCEMAPS.md
new file mode 100644
index 000000000..525f055ac
--- /dev/null
+++ b/SOURCEMAPS.md
@@ -0,0 +1,60 @@
+# Sourcemaps
+
+Today, we expect sourcemaps to work in ember-cli. A default ember-cli
+application should have functioning sourcemaps, with the exception of in-module
+ES6 -> ES5 mappings.  This is for two reasons:
+
+1. dramatically increases build costs
+2. babel (at least at the time) does not correctly mangle names, which results in
+   a very poor debugging experience when some of your variables are debuggable
+   and others are not.  We hope this is eventually rectified.
+
+ember-cli, via ef4/fast-sourcemap-concat, attempts to detect invalid sourcemaps,
+and discards them while still producing a valid, correct but incomplete
+sourcemap.  This means inputs with invalid sourcemaps can still be viewed
+separately in the chrome debugger (as separate "files"), but will have no
+mappings (eg for function names).
+
+If your experience differs from the above, please read the rest of
+this document before logging an issue.
+
+## Debugging Sourcemaps
+
+Sourcemaps can be a bit tricky to read. This is due to them being VLQ encoded,
+and humans not being very good at decoding VLQ on the fly.
+
+There are broadly two ways a source file will have sourcemaps attached.
+
+1. Via an external map file
+2. Inline, in Base64.
+
+External sourcemap files can be read with the [helper tools at
+fast-source-map](https://github.com/krisselden/fast-source-map/tree/master/bin).
+
+Inline sourcemaps will look something like
+
+```
+//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImlubmVyL2ZpcnN0LmpzIiwiaW5uZXIvc2Vjb25kLmpzIl0sInNvdXJjZXNDb250ZW50IjpbImZ1bmN0aW9uIG1lYW5pbmdPZkxpZmUoKSB7XG4gIHRocm93IG5ldyBFcnJvcig0Mik7XG59XG5cbmZ1bmN0aW9uIGJvb20oKSB7XG4gIHRocm93IG5ldyBFcnJvcignYm9vbScpO1xufVxuIiwiZnVuY3Rpb24gc29tZXRoaW5nRWxzZSgpIHtcbiAgdGhyb3cgbmV3IEVycm9yKFwic29tZXRoaWduIGVsc2VcIik7XG59XG4iXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FDTkE7QUFDQTtBQUNBOyIsImZpbGUiOiJhbGwtaW5uZXIuanMifQ==
+```
+
+
+The easiest way to read them is to first convert them to an external map file.
+To do so:
+
+1. take the part after `base64,` and in node `new Buffer(string,
+   'base64').toString();`
+2. write this to a file, and one can now use the above described fast sourcemap
+   helpers
+
+### Handling Invalid Sourcemaps
+
+Add `DEBUG='fast-sourcemap-concat:*'` to see error output when invalid
+sourcemaps are detected.
+
+## Useful tools
+
+- source map visualizer - https://sokra.github.io/source-map-visualization/ **
+  Very useful, but appears to have issues with both large sourcemaps, and
+  incorrect sourcemaps (such as ones with unexpected negative offsets).
+- encode/decode - https://github.com/krisselden/fast-source-map/tree/master/bin
+- srcmap r3 - `https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit`
diff --git a/lib/broccoli/ember-app.js b/lib/broccoli/ember-app.js
index 55e2af95d..624d7df47 100644
--- a/lib/broccoli/ember-app.js
+++ b/lib/broccoli/ember-app.js
@@ -296,8 +296,7 @@ EmberApp.prototype._initVendorFiles = function() {
   }
 
   var handlebarsVendorFiles;
-  var bowerDeps = this.project.bowerDependencies();
-  if ('handlebars' in bowerDeps) {
+  if ('handlebars' in this.project.bowerDependencies()) {
     handlebarsVendorFiles = {
       development: this.bowerDirectory + '/handlebars/handlebars.js',
       production:  this.bowerDirectory + '/handlebars/handlebars.runtime.js'
@@ -327,18 +326,13 @@ EmberApp.prototype._initVendorFiles = function() {
     ]
   }, this.options.vendorFiles), isNull);
 
-  if (this._addonInstalled('ember-resolver') || !bowerDeps['ember-resolver']) {
+  if (this.registry.availablePlugins['ember-resolver']) {
     // if the project is using `ember-resolver` as an addon
     // remove it from `vendorFiles` (the NPM version properly works
     // without `app.import`s)
     delete this.vendorFiles['ember-resolver.js'];
   }
 
-  // don't crash if ember-cli-shims is not included
-  if (!bowerDeps['ember-cli-shims']) {
-    delete this.vendorFiles['app-shims.js'];
-  }
-
   // this is needed to support versions of Ember older than
   // 1.8.0 (when ember-testing.js was added to the deployment)
   if (this.vendorFiles['ember-testing.js'] && !existsSync(this.vendorFiles['ember-testing.js'][0])) {
diff --git a/lib/cli/index.js b/lib/cli/index.js
index ce74fb459..5ec9f8411 100644
--- a/lib/cli/index.js
+++ b/lib/cli/index.js
@@ -42,7 +42,7 @@ function clientId() {
   if (id) {
     return id;
   } else {
-    id = require('uuid').v4().toString();
+    id = require('node-uuid').v4().toString();
     configStore.set('client-id', id);
     return id;
   }
@@ -58,7 +58,7 @@ function configureLogger(env) {
 
 // Options: Array cliArgs, Stream inputStream, Stream outputStream
 module.exports = function(options) {
-  var UI = options.UI || require('console-ui');
+  var UI = options.UI || require('../ui');
   var Yam = options.Yam || require('yam');
   var CLI = require('./cli');
   var Leek = options.Leek || require('leek');
diff --git a/lib/models/blueprint.js b/lib/models/blueprint.js
index ba05be120..0d9e5082b 100644
--- a/lib/models/blueprint.js
+++ b/lib/models/blueprint.js
@@ -221,7 +221,8 @@ var Blueprint = CoreObject.extend({
   },
 
   /**
-    Used to retrieve files for blueprint.
+    Used to retrieve files for blueprint. The `file` param is an
+    optional string that is turned into a glob.
 
     @method files
     @return {Array} Contents of the blueprint's files directory
diff --git a/lib/models/file-info.js b/lib/models/file-info.js
index 8344c6f6d..c9e514f68 100644
--- a/lib/models/file-info.js
+++ b/lib/models/file-info.js
@@ -9,7 +9,15 @@ var EditFileDiff = require('./edit-file-diff');
 var EOL          = require('os').EOL;
 var isBinaryFile = require('isbinaryfile').sync;
 var canEdit      = require('../utilities/open-editor').canEdit;
-var processTemplate = require('../utilities/process-template');
+
+function processTemplate(content, context) {
+  var options = {
+    evaluate:    /<%([\s\S]+?)%>/g,
+    interpolate: /<%=([\s\S]+?)%>/g,
+    escape:      /<%-([\s\S]+?)%>/g
+  };
+  return require('lodash.template')(content, options)(context);
+}
 
 function diffHighlight(line) {
   if (line[0] === '+') {
diff --git a/lib/models/project.js b/lib/models/project.js
index 8c9046ac7..35b7efde0 100644
--- a/lib/models/project.js
+++ b/lib/models/project.js
@@ -686,7 +686,7 @@ function ensureUI(_ui) {
 
   if (!ui) {
     // TODO: one UI (lib/cli/index.js also has one for now...)
-    var UI = require('console-ui');
+    var UI = require('../ui');
     ui = new UI({
       inputStream:  process.stdin,
       outputStream: process.stdout,
diff --git a/lib/ui/index.js b/lib/ui/index.js
new file mode 100644
index 000000000..4be61aaba
--- /dev/null
+++ b/lib/ui/index.js
@@ -0,0 +1,237 @@
+'use strict';
+
+var ora              = require('ora');
+var EOL              = require('os').EOL;
+var chalk            = require('chalk');
+var writeError       = require('./write-error');
+
+var DEFAULT_WRITE_LEVEL = 'INFO';
+
+// Note: You should use `ui.outputStream`, `ui.inputStream` and `ui.write()`
+//       instead of `process.stdout` and `console.log`.
+//       Thus the pleasant progress indicator automatically gets
+//       interrupted and doesn't mess up the output! -> Convenience :P
+
+module.exports = UI;
+
+/**
+  The UI provides the CLI with a unified mechanism for providing output and
+  requesting input from the user. This becomes useful when wanting to adjust
+  logLevels, or mock input/output for tests.
+
+  @class UI
+  @constructor
+  @param {Object} options Configuration options
+
+  @example
+  ```js
+  new UI({
+    inputStream: process.stdin,
+    outputStream: process.stdout,
+    writeLevel: 'DEBUG' | 'INFO' | 'WARNING' | 'ERROR',
+    ci: true | false
+  });
+  ```
+*/
+function UI(options) {
+  var spinner = this.spinner = ora({ color: 'green' });
+
+  this.through  = require('through');
+
+  // Output stream
+  this.actualOutputStream = options.outputStream;
+  this.outputStream = this.through(function(data) {
+    spinner.stop();
+    this.emit('data', data);
+  });
+
+  this.outputStream.setMaxListeners(0);
+  this.outputStream.pipe(this.actualOutputStream);
+
+  this.inputStream = options.inputStream;
+  this.errorStream = options.errorStream;
+
+  this.errorLog = options.errorLog || [];
+  this.writeLevel = options.writeLevel || DEFAULT_WRITE_LEVEL;
+  this.ci = !!options.ci;
+}
+
+/**
+  Unified mechanism to write a string to the console.
+  Optionally include a writeLevel, this is used to decide if the specific
+  logging mechanism should or should not be printed.
+
+  @method write
+  @param {String} data
+  @param {String} [writeLevel='INFO']
+*/
+UI.prototype.write = function(data, writeLevel) {
+  if (writeLevel === 'ERROR') {
+    this.errorStream.write(data);
+  } else if (this.writeLevelVisible(writeLevel)) {
+    this.outputStream.write(data);
+  }
+};
+
+/**
+  Unified mechanism to write a string and new line to the console.
+  Optionally include a writeLevel, this is used to decide if the specific
+  logging mechanism should or should not be printed.
+  @method writeLine
+  @param {String} data
+  @param {String} [writeLevel='INFO']
+*/
+UI.prototype.writeLine = function(data, writeLevel) {
+  this.write(data + EOL, writeLevel);
+};
+
+/**
+  Helper method to write a string with the DEBUG writeLevel and gray chalk
+  @method writeDebugLine
+  @param {String} data
+*/
+UI.prototype.writeDebugLine = function(data) {
+  this.writeLine(chalk.gray(data), 'DEBUG');
+};
+
+/**
+  Helper method to write a string with the INFO writeLevel and cyan chalk
+  @method writeInfoLine
+  @param {String} data
+*/
+UI.prototype.writeInfoLine = function(data) {
+  this.writeLine(chalk.cyan(data), 'INFO');
+};
+
+/**
+  Helper method to write a string with the WARNING writeLevel and yellow chalk.
+  Optionally include a test. If falsy, the warning will be printed. By default, warnings
+  will be prepended with WARNING text when printed.
+  @method writeWarnLine
+  @param {String} data
+  @param {Boolean} test
+  @param {Boolean} prepend
+*/
+UI.prototype.writeWarnLine = function(data, test, prepend) {
+  if (test) { return; }
+
+  data = this.prependLine('WARNING', data, prepend);
+  this.writeLine(chalk.yellow(data), 'WARNING', test);
+};
+
+/**
+  Helper method to write a string with the WARNING writeLevel and yellow chalk.
+  Optionally include a test. If falsy, the deprecation will be printed. By default deprecations
+  will be prepended with DEPRECATION text when printed.
+  @method writeDeprecateLine
+  @param {String} data
+  @param {Boolean} test
+  @param {Boolean} prepend
+*/
+UI.prototype.writeDeprecateLine = function(data, test, prepend) {
+  data = this.prependLine('DEPRECATION', data, prepend);
+  this.writeWarnLine(data, test, false);
+};
+
+/**
+  Utility method to prepend a line with a flag-like string (i.e., WARNING).
+  @method prependLine
+  @param {String} prependData
+  @param {String} data
+  @param {Boolean} prepend
+*/
+UI.prototype.prependLine = function(prependData, data, prepend) {
+  if (typeof prepend === 'undefined' || prepend) {
+    data = prependData + ': ' + data;
+  }
+
+  return data;
+};
+
+/**
+  Unified mechanism to an Error to the console.
+  This will occure at a writeLevel of ERROR
+
+  @method writeError
+  @param {Error} error
+*/
+UI.prototype.writeError = function(error) {
+  writeError(this, error);
+};
+
+/**
+  Sets the write level for the UI. Valid write levels are 'DEBUG', 'INFO',
+  'WARNING', and 'ERROR'.
+
+  @method setWriteLevel
+  @param {String} level
+*/
+UI.prototype.setWriteLevel = function(level) {
+  if (Object.keys(this.WRITE_LEVELS).indexOf(level) === -1) {
+    throw new Error('Unknown write level. Valid values are \'DEBUG\', \'INFO\', \'WARNING\', and \'ERROR\'.');
+  }
+
+  this.writeLevel = level;
+};
+
+UI.prototype.startProgress = function(message/*, stepString*/) {
+  if (this.writeLevelVisible('INFO')) {
+    if (this.ci) {
+      this.writeLine(message);
+    } else {
+      this.spinner.text = message;
+      this.spinner.start();
+    }
+  }
+};
+
+UI.prototype.stopProgress = function() {
+  if (this.writeLevelVisible('INFO') && !this.ci) {
+    this.spinner.stop();
+  }
+};
+
+/**
+  Launch the prompt interface (inquiry session) with (Array of Questions || Question)
+
+  See [Inquirer.js#question](https://github.com/SBoudrias/Inquirer.js#question) for Question {Object} properties
+
+  @method prompt
+  @param {Object} questions
+  @param {Function} [callback]
+  @return {Promise}
+
+ */
+UI.prototype.prompt = function(questions, callback) {
+  var inquirer = require('inquirer');
+
+  // If no callback was provided, automatically return a promise
+  return inquirer.prompt(questions, callback);
+};
+
+/**
+  @property WRITE_LEVELS
+  @private
+  @type Object
+*/
+UI.prototype.WRITE_LEVELS = {
+  'DEBUG': 1,
+  'INFO': 2,
+  'WARNING': 3,
+  'ERROR': 4
+};
+
+/**
+  Whether or not the specified write level should be printed by this UI.
+
+  @method writeLevelVisible
+  @private
+  @param {String} writeLevel
+  @return {Boolean}
+*/
+UI.prototype.writeLevelVisible = function(writeLevel) {
+  var levels = this.WRITE_LEVELS;
+  writeLevel = writeLevel || DEFAULT_WRITE_LEVEL;
+
+  return levels[writeLevel] >= levels[this.writeLevel];
+};
diff --git a/lib/ui/write-error.js b/lib/ui/write-error.js
new file mode 100644
index 000000000..5114f9da4
--- /dev/null
+++ b/lib/ui/write-error.js
@@ -0,0 +1,25 @@
+'use strict';
+var chalk = require('chalk');
+
+module.exports = function writeError(ui, error) {
+  if (!error) { return; }
+
+  // Uglify errors have a filename instead
+  var fileName = error.file || error.filename;
+  if (fileName) {
+    if (error.line) {
+      fileName += error.col ? ' (' + error.line + ':' + error.col + ')' : ' (' + error.line + ')';
+    }
+    ui.writeLine(chalk.red('File: ' + fileName), 'ERROR');
+  }
+
+  if (error.message) {
+    ui.writeLine(chalk.red(error.message), 'ERROR');
+  } else {
+    ui.writeLine(chalk.red(error), 'ERROR');
+  }
+
+  if (error.stack) {
+    ui.writeLine(error.stack, 'ERROR');
+  }
+};
diff --git a/lib/utilities/process-template.js b/lib/utilities/process-template.js
deleted file mode 100644
index 05e197fc1..000000000
--- a/lib/utilities/process-template.js
+++ /dev/null
@@ -1,8 +0,0 @@
-module.exports = function processTemplate(content, context) {
-  var options = {
-    evaluate:    /<%([\s\S]+?)%>/g,
-    interpolate: /<%=([\s\S]+?)%>/g,
-    escape:      /<%-([\s\S]+?)%>/g
-  };
-  return require('lodash.template')(content, options)(context);
-}
diff --git a/package.json b/package.json
index e0b153bd9..49dfbe38b 100644
--- a/package.json
+++ b/package.json
@@ -52,12 +52,11 @@
     "broccoli-middleware": "^0.18.1",
     "broccoli-source": "^1.1.0",
     "broccoli-stew": "^1.2.0",
-    "capture-exit": "^1.0.4",
+    "capture-exit": "^1.0.3",
     "chalk": "^1.1.3",
     "clean-base-url": "^1.0.0",
     "compression": "^1.4.4",
     "configstore": "^2.0.0",
-    "console-ui": "^1.0.2",
     "core-object": "^2.0.2",
     "diff": "^1.3.1",
     "ember-cli-broccoli-sane-watcher": "^2.0.3",
@@ -95,6 +94,7 @@
     "minimatch": "^3.0.0",
     "morgan": "^1.5.2",
     "node-modules-path": "^1.0.0",
+    "node-uuid": "^1.4.3",
     "nopt": "^3.0.1",
     "npm": "3.10.8",
     "npm-package-arg": "^4.1.1",
@@ -114,7 +114,6 @@
     "through": "^2.3.6",
     "tiny-lr": "^1.0.3",
     "tree-sync": "^1.1.4",
-    "uuid": "^3.0.0",
     "walk-sync": "^0.3.0",
     "yam": "0.0.22"
   },
@@ -150,7 +149,6 @@
       "find-up",
       "mocha-eslint",
       "nock",
-      "npm",
       "ora"
     ]
   }


Test patch:
diff --git a/tests/factories/command-options.js b/tests/factories/command-options.js
index fcf312feb..77826fd15 100644
--- a/tests/factories/command-options.js
+++ b/tests/factories/command-options.js
@@ -1,7 +1,7 @@
 'use strict';
 
 var defaults      = require('ember-cli-lodash-subset').defaults;
-var MockUI        = require('console-ui/mock');
+var MockUI        = require('../helpers/mock-ui');
 var MockAnalytics = require('../helpers/mock-analytics');
 var MockProject   = require('../helpers/mock-project');
 
diff --git a/tests/helpers/ember.js b/tests/helpers/ember.js
index e3b2f7ce7..13be3b0f8 100644
--- a/tests/helpers/ember.js
+++ b/tests/helpers/ember.js
@@ -1,6 +1,6 @@
 'use strict';
 
-var MockUI        = require('console-ui/mock');
+var MockUI        = require('./mock-ui');
 var MockAnalytics = require('./mock-analytics');
 var cli           = require('../../lib/cli');
 var path          = require('path');
diff --git a/tests/helpers/mock-project.js b/tests/helpers/mock-project.js
index 89ed75895..0f0fd25c4 100644
--- a/tests/helpers/mock-project.js
+++ b/tests/helpers/mock-project.js
@@ -1,7 +1,7 @@
 'use strict';
 
 var Project = require('../../lib/models/project');
-var MockUI  = require('console-ui/mock');
+var MockUI        = require('../helpers/mock-ui');
 
 function MockProject() {
   var root = process.cwd();
diff --git a/tests/helpers/mock-ui.js b/tests/helpers/mock-ui.js
new file mode 100644
index 000000000..42aa83013
--- /dev/null
+++ b/tests/helpers/mock-ui.js
@@ -0,0 +1,33 @@
+'use strict';
+
+var UI      = require('../../lib/ui');
+var through = require('through');
+var Promise = require('../../lib/ext/promise');
+
+module.exports = MockUI;
+function MockUI(options) {
+  UI.call(this, {
+    inputStream: through(),
+    outputStream: through(function(data) {
+      if (options && options.outputStream) {
+        options.outputStream.push(data);
+      }
+      this.output += data;
+    }.bind(this)),
+    errorStream: through(function(data) {
+      this.errors += data;
+    }.bind(this))
+  });
+
+  this.output = '';
+  this.errors = '';
+  this.errorLog = options && options.errorLog || [];
+}
+
+MockUI.prototype = Object.create(UI.prototype);
+MockUI.prototype.constructor = MockUI;
+MockUI.prototype.clear = function() {
+  this.output = '';
+  this.errors = '';
+  this.errorLog = [];
+};
diff --git a/tests/runner.js b/tests/runner.js
index 95a101a63..937cee04e 100644
--- a/tests/runner.js
+++ b/tests/runner.js
@@ -12,7 +12,7 @@ if (process.env.EOLNEWLINE) {
 
 fs.removeSync('.deps-tmp');
 
-var root = 'tests/{unit,integration,acceptance}';
+var root = 'tests/{unit,acceptance}';
 var _checkOnlyInTests = RSVP.denodeify(mochaOnlyDetector.checkFolder.bind(null, root + '/**/*{-test,-slow}.js'));
 var optionOrFile = process.argv[2];
 var mocha = new Mocha({
diff --git a/tests/unit/analytics-test.js b/tests/unit/analytics-test.js
index 854e39925..3bab677ff 100644
--- a/tests/unit/analytics-test.js
+++ b/tests/unit/analytics-test.js
@@ -2,7 +2,7 @@
 
 var expect = require('chai').expect;
 var Command = require('../../lib/models/command');
-var MockUI = require('console-ui/mock');
+var MockUI = require('../helpers/mock-ui');
 var MockProject = require('../helpers/mock-project');
 var command;
 var called = false;
diff --git a/tests/unit/broccoli/ember-app-test.js b/tests/unit/broccoli/ember-app-test.js
index 8c6ae2471..247b70f00 100644
--- a/tests/unit/broccoli/ember-app-test.js
+++ b/tests/unit/broccoli/ember-app-test.js
@@ -9,7 +9,7 @@ var expect     = require('chai').expect;
 var proxyquire = require('proxyquire');
 var td = require('testdouble');
 
-var MockUI = require('console-ui/mock');
+var MockUI = require('../../helpers/mock-ui');
 
 var mergeTreesStub;
 var EmberApp = proxyquire('../../../lib/broccoli/ember-app', {
@@ -177,47 +177,6 @@ describe('broccoli/ember-app', function() {
         }).to.not.throw(/loader.js addon is missing/);
       });
     });
-
-    describe('ember-resolver NPM vs Bower', function() {
-      it('does not load ember-resolver.js as bower dep when ember-resolver is present in registry.availablePlugins', function() {
-        var app = new EmberApp({ project: project });
-        expect(app.vendorFiles['ember-resolver']).to.equal(undefined);
-      });
-
-      it('keeps ember-resolver.js in vendorFiles when NPM ember-resolver is not installed, but is present in bower.json', function() {
-        project.bowerDependencies = function() { return { 'ember': {}, 'ember-resolver': {} }; }
-        var app = new EmberApp({
-          project: project,
-          registry: {
-            add: function() { },
-            availablePlugins: { 'loader.js': {} }
-          }
-        });
-
-        expect(app.vendorFiles['ember-resolver'][0]).to.equal('bower_components/ember-resolver/dist/modules/ember-resolver.js');
-      });
-
-      it('removes ember-resolver.js from vendorFiles when not in bower.json and NPM ember-resolver not installed', function() {
-        project.bowerDependencies = function() { return { 'ember': {} }; }
-        var app = new EmberApp({
-          project: project,
-          registry: {
-            add: function() { },
-            availablePlugins: { 'loader.js': {} }
-          }
-        });
-
-        expect(app.vendorFiles['ember-resolver']).to.equal(undefined);
-      });
-    });
-
-    describe('ember-cli-shims', function() {
-      it('removes app-shims.js from vendorFiles when ember-cli-shims is not in bower.json', function() {
-        project.bowerDependencies = function() { return { 'ember': {} }; }
-        var app = new EmberApp({ project: project });
-        expect(app.vendorFiles['app-shims.js']).to.equal(undefined);
-      });
-    });
   });
 
   describe('contentFor', function() {
diff --git a/tests/unit/broccoli/merge-trees-test.js b/tests/unit/broccoli/merge-trees-test.js
index 5c56c21fc..f2984adb9 100644
--- a/tests/unit/broccoli/merge-trees-test.js
+++ b/tests/unit/broccoli/merge-trees-test.js
@@ -7,7 +7,7 @@ var expect     = require('chai').expect;
 var proxyquire = require('proxyquire');
 var td = require('testdouble');
 
-var MockUI = require('console-ui/mock');
+var MockUI = require('../../helpers/mock-ui');
 
 var mergeTreesStub;
 var mergeTrees = proxyquire('../../../lib/broccoli/merge-trees', {
diff --git a/tests/unit/cli/cli-test.js b/tests/unit/cli/cli-test.js
index 6de37c7c5..a223d7890 100644
--- a/tests/unit/cli/cli-test.js
+++ b/tests/unit/cli/cli-test.js
@@ -1,7 +1,7 @@
 'use strict';
 
 var expect        = require('chai').expect;
-var MockUI        = require('console-ui/mock');
+var MockUI        = require('../../helpers/mock-ui');
 var MockAnalytics = require('../../helpers/mock-analytics');
 var CLI           = require('../../../lib/cli/cli');
 var td = require('testdouble');
diff --git a/tests/unit/cli/lookup-command-test.js b/tests/unit/cli/lookup-command-test.js
index 48a484231..d2d07cb4d 100644
--- a/tests/unit/cli/lookup-command-test.js
+++ b/tests/unit/cli/lookup-command-test.js
@@ -4,7 +4,7 @@ var expect        = require('chai').expect;
 var lookupCommand = require('../../../lib/cli/lookup-command');
 var Command       = require('../../../lib/models/command');
 var Project       = require('../../../lib/models/project');
-var MockUI        = require('console-ui/mock');
+var MockUI        = require('../../helpers/mock-ui');
 var AddonCommand  = require('../../fixtures/addon/commands/addon-command');
 var OtherCommand  = require('../../fixtures/addon/commands/other-addon-command');
 var ClassCommand  = require('../../fixtures/addon/commands/addon-command-class');
diff --git a/tests/unit/commands/init-test.js b/tests/unit/commands/init-test.js
index 42226d89d..aa87fa1b0 100644
--- a/tests/unit/commands/init-test.js
+++ b/tests/unit/commands/init-test.js
@@ -5,7 +5,7 @@ var os            = require('os');
 var path          = require('path');
 var expect        = require('chai').expect;
 var map           = require('ember-cli-lodash-subset').map;
-var MockUI        = require('console-ui/mock');
+var MockUI        = require('../../helpers/mock-ui');
 var MockAnalytics = require('../../helpers/mock-analytics');
 var Promise       = require('../../../lib/ext/promise');
 var Blueprint     = require('../../../lib/models/blueprint');
diff --git a/tests/unit/models/addon-discovery-test.js b/tests/unit/models/addon-discovery-test.js
index 10df312f4..78c5b1b5a 100644
--- a/tests/unit/models/addon-discovery-test.js
+++ b/tests/unit/models/addon-discovery-test.js
@@ -6,7 +6,7 @@ var assign = require('ember-cli-lodash-subset').assign;
 var Project = require('../../../lib/models/project');
 var AddonDiscovery = require('../../../lib/models/addon-discovery');
 var fixturePath = path.resolve(__dirname, '../../fixtures/addon');
-var MockUI = require('console-ui/mock');
+var MockUI = require('../../helpers/mock-ui');
 var chalk = require('chalk');
 
 describe('models/addon-discovery.js', function() {
diff --git a/tests/unit/models/addon-test.js b/tests/unit/models/addon-test.js
index 9d0a90908..0e433d65d 100644
--- a/tests/unit/models/addon-test.js
+++ b/tests/unit/models/addon-test.js
@@ -8,7 +8,7 @@ var Promise = require('../../../lib/ext/promise');
 var expect  = require('chai').expect;
 var remove  = Promise.denodeify(fs.remove);
 var findWhere = require('ember-cli-lodash-subset').find;
-var MockUI = require('console-ui/mock');
+var MockUI = require('../../helpers/mock-ui');
 var mkTmpDirIn = require('../../../lib/utilities/mk-tmp-dir-in');
 
 var broccoli  = require('broccoli-builder');
diff --git a/tests/unit/models/asset-size-printer-test.js b/tests/unit/models/asset-size-printer-test.js
index a5f362bb4..de5b6fba8 100644
--- a/tests/unit/models/asset-size-printer-test.js
+++ b/tests/unit/models/asset-size-printer-test.js
@@ -1,7 +1,7 @@
 'use strict';
 
 var expect           = require('chai').expect;
-var MockUi           = require('console-ui/mock');
+var MockUi           = require('../../helpers/mock-ui');
 var AssetSizePrinter = require('../../../lib/models/asset-size-printer');
 var Promise          = require('../../../lib/ext/promise');
 var path             = require('path');
diff --git a/tests/unit/models/blueprint-test.js b/tests/unit/models/blueprint-test.js
index a2618aaa3..f1bb6c37e 100644
--- a/tests/unit/models/blueprint-test.js
+++ b/tests/unit/models/blueprint-test.js
@@ -4,7 +4,7 @@
 var fs                = require('fs-extra');
 var Task              = require('../../../lib/models/task');
 var MockProject       = require('../../helpers/mock-project');
-var MockUI            = require('console-ui/mock');
+var MockUI            = require('../../helpers/mock-ui');
 var processHelpString = require('../../helpers/process-help-string');
 var expect            = require('chai').expect;
 var path              = require('path');
diff --git a/tests/unit/models/file-info-test.js b/tests/unit/models/file-info-test.js
index d2d0b7a37..3fde0440b 100644
--- a/tests/unit/models/file-info-test.js
+++ b/tests/unit/models/file-info-test.js
@@ -1,7 +1,7 @@
 'use strict';
 
 var expect    = require('chai').expect;
-var MockUI    = require('console-ui/mock');
+var MockUI    = require('../../helpers/mock-ui');
 var FileInfo  = require('../../../lib/models/file-info');
 var path      = require('path');
 var fs        = require('fs-extra');
diff --git a/tests/unit/models/project-test.js b/tests/unit/models/project-test.js
index 7c4a333a1..34545cce9 100644
--- a/tests/unit/models/project-test.js
+++ b/tests/unit/models/project-test.js
@@ -6,7 +6,7 @@ var Addon           = require('../../../lib/models/addon');
 var tmp             = require('../../helpers/tmp');
 var touch           = require('../../helpers/file-utils').touch;
 var expect          = require('chai').expect;
-var MockUI          = require('console-ui/mock');
+var MockUI          = require('../../helpers/mock-ui');
 var emberCLIVersion = require('../../../lib/utilities/version-utils').emberCLIVersion;
 var td = require('testdouble')
 
diff --git a/tests/unit/models/server-watcher-test.js b/tests/unit/models/server-watcher-test.js
index 9530b65de..0f780da15 100644
--- a/tests/unit/models/server-watcher-test.js
+++ b/tests/unit/models/server-watcher-test.js
@@ -2,7 +2,7 @@
 
 var expect = require('chai').expect;
 var EOL = require('os').EOL;
-var MockUI = require('console-ui/mock');
+var MockUI = require('../../helpers/mock-ui');
 var MockAnalytics = require('../../helpers/mock-analytics');
 var MockServerWatcher  = require('../../helpers/mock-watcher');
 var ServerWatcher = require('../../../lib/models/server-watcher');
diff --git a/tests/unit/models/watch-detector-test.js b/tests/unit/models/watch-detector-test.js
index 902c2baa0..6cda4eaa9 100644
--- a/tests/unit/models/watch-detector-test.js
+++ b/tests/unit/models/watch-detector-test.js
@@ -2,7 +2,7 @@
 
 var expect = require('chai').expect;
 
-var MockUI = require('console-ui/mock');
+var MockUI = require('../../helpers/mock-ui');
 var WatchDetector = require('../../../lib/models/watch-detector');
 var EOL = require('os').EOL;
 var chalk = require('chalk');
diff --git a/tests/unit/models/watcher-test.js b/tests/unit/models/watcher-test.js
index 1d5d5f430..ef134ba9b 100644
--- a/tests/unit/models/watcher-test.js
+++ b/tests/unit/models/watcher-test.js
@@ -2,7 +2,7 @@
 
 var expect = require('chai').expect;
 
-var MockUI = require('console-ui/mock');
+var MockUI = require('../../helpers/mock-ui');
 var MockAnalytics = require('../../helpers/mock-analytics');
 var MockWatcher  = require('../../helpers/mock-watcher');
 var Watcher = require('../../../lib/models/watcher');
diff --git a/tests/unit/settings-file/leek-options-test.js b/tests/unit/settings-file/leek-options-test.js
index e028b7819..7f4b3135c 100644
--- a/tests/unit/settings-file/leek-options-test.js
+++ b/tests/unit/settings-file/leek-options-test.js
@@ -1,7 +1,7 @@
 'use strict';
 
 var expect         = require('chai').expect;
-var MockUI         = require('console-ui/mock');
+var MockUI         = require('../../helpers/mock-ui');
 var Yam            = require('yam');
 var cliEntry       = require('../../../lib/cli');
 
diff --git a/tests/unit/settings-file/precedence-settings-test.js b/tests/unit/settings-file/precedence-settings-test.js
index d7c2d0981..5cea203a9 100644
--- a/tests/unit/settings-file/precedence-settings-test.js
+++ b/tests/unit/settings-file/precedence-settings-test.js
@@ -2,7 +2,7 @@
 
 var expect         = require('chai').expect;
 var merge          = require('ember-cli-lodash-subset').merge;
-var MockUI         = require('console-ui/mock');
+var MockUI         = require('../../helpers/mock-ui');
 var MockAnalytics  = require('../../helpers/mock-analytics');
 var Command        = require('../../../lib/models/command');
 var Yam            = require('yam');
diff --git a/tests/unit/tasks/addon-install-test.js b/tests/unit/tasks/addon-install-test.js
index 8357e8a05..28a0742b4 100644
--- a/tests/unit/tasks/addon-install-test.js
+++ b/tests/unit/tasks/addon-install-test.js
@@ -1,7 +1,7 @@
 'use strict';
 
 var AddonInstallTask = require('../../../lib/tasks/addon-install');
-var MockProject      = require('console-ui/mock');
+var MockProject      = require('../../helpers/mock-project');
 var expect           = require('chai').expect;
 var CoreObject       = require('core-object');
 var Promise          = require('rsvp').Promise;
diff --git a/tests/unit/tasks/bower-install-test.js b/tests/unit/tasks/bower-install-test.js
index 2208b9717..09a1de969 100644
--- a/tests/unit/tasks/bower-install-test.js
+++ b/tests/unit/tasks/bower-install-test.js
@@ -1,7 +1,7 @@
 'use strict';
 
 var BowerInstallTask = require('../../../lib/tasks/bower-install');
-var MockUI           = require('console-ui/mock');
+var MockUI           = require('../../helpers/mock-ui');
 var MockProject      = require('../../helpers/mock-project');
 var expect           = require('chai').expect;
 
diff --git a/tests/unit/tasks/git-init-test.js b/tests/unit/tasks/git-init-test.js
index 4a4ac00be..c9e8254e7 100644
--- a/tests/unit/tasks/git-init-test.js
+++ b/tests/unit/tasks/git-init-test.js
@@ -2,7 +2,7 @@
 
 var fs          = require('fs-extra');
 var expect      = require('chai').expect;
-var MockUI      = require('console-ui/mock');
+var MockUI      = require('../../helpers/mock-ui');
 var GitInitTask = require('../../../lib/tasks/git-init');
 var MockProject = require('../../helpers/mock-project');
 var Promise     = require('../../../lib/ext/promise');
diff --git a/tests/unit/tasks/npm-install-test.js b/tests/unit/tasks/npm-install-test.js
index 3d11bd233..ea8747c3f 100644
--- a/tests/unit/tasks/npm-install-test.js
+++ b/tests/unit/tasks/npm-install-test.js
@@ -1,7 +1,7 @@
 'use strict';
 
 var NpmInstallTask = require('../../../lib/tasks/npm-install');
-var MockUI         = require('console-ui/mock');
+var MockUI         = require('../../helpers/mock-ui');
 var expect         = require('chai').expect;
 
 describe('npm install task', function() {
diff --git a/tests/unit/tasks/server/express-server-test.js b/tests/unit/tasks/server/express-server-test.js
index eff205a1e..1973725c9 100644
--- a/tests/unit/tasks/server/express-server-test.js
+++ b/tests/unit/tasks/server/express-server-test.js
@@ -3,7 +3,7 @@
 var expect            = require('chai').expect;
 var ExpressServer     = require('../../../../lib/tasks/server/express-server');
 var Promise           = require('../../../../lib/ext/promise');
-var MockUI            = require('console-ui/mock');
+var MockUI            = require('../../../helpers/mock-ui');
 var MockProject       = require('../../../helpers/mock-project');
 var MockWatcher       = require('../../../helpers/mock-watcher');
 var MockServerWatcher = require('../../../helpers/mock-server-watcher');
diff --git a/tests/unit/tasks/server/livereload-server-test.js b/tests/unit/tasks/server/livereload-server-test.js
index 4aa1fb19a..d94f13699 100644
--- a/tests/unit/tasks/server/livereload-server-test.js
+++ b/tests/unit/tasks/server/livereload-server-test.js
@@ -2,7 +2,7 @@
 
 var expect            = require('chai').expect;
 var LiveReloadServer  = require('../../../../lib/tasks/server/livereload-server');
-var MockUI            = require('console-ui/mock');
+var MockUI            = require('../../../helpers/mock-ui');
 var MockExpressServer = require('../../../helpers/mock-express-server');
 var net               = require('net');
 var EOL               = require('os').EOL;
diff --git a/tests/unit/tasks/test-server-test.js b/tests/unit/tasks/test-server-test.js
index 7dbeb9098..4274a18d1 100644
--- a/tests/unit/tasks/test-server-test.js
+++ b/tests/unit/tasks/test-server-test.js
@@ -4,7 +4,7 @@ var expect         = require('chai').expect;
 var SilentError    = require('silent-error');
 var TestServerTask = require('../../../lib/tasks/test-server');
 var MockProject    = require('../../helpers/mock-project');
-var MockUI         = require('console-ui/mock');
+var MockUI         = require('../../helpers/mock-ui');
 var MockWatcher    = require('../../helpers/mock-watcher');
 
 describe('test server', function() {
diff --git a/tests/unit/tasks/update-test.js b/tests/unit/tasks/update-test.js
index 6530970d8..7ac1dc3e7 100644
--- a/tests/unit/tasks/update-test.js
+++ b/tests/unit/tasks/update-test.js
@@ -3,7 +3,7 @@
 var fs         = require('fs-extra');
 var path       = require('path');
 var expect     = require('chai').expect;
-var MockUI     = require('console-ui/mock');
+var MockUI     = require('../../helpers/mock-ui');
 var Promise    = require('../../../lib/ext/promise');
 var UpdateTask = require('../../../lib/tasks/update');
 
diff --git a/tests/unit/ui/index-test.js b/tests/unit/ui/index-test.js
new file mode 100644
index 000000000..7e2298fe4
--- /dev/null
+++ b/tests/unit/ui/index-test.js
@@ -0,0 +1,85 @@
+'use strict';
+
+var expect = require('chai').expect;
+var MockUI = require('../../helpers/mock-ui');
+var EOL    = require('os').EOL;
+var chalk  = require('chalk');
+
+describe('UI', function() {
+  var ui;
+
+  beforeEach(function() {
+    ui = new MockUI();
+  });
+
+  describe('writeDebugLine', function() {
+    it('does not write at the default level', function() {
+      ui.writeDebugLine('foo');
+      expect(ui.output).to.equal('');
+    });
+
+    it('writes in the correct chalk', function() {
+      ui.writeLevel = 'DEBUG';
+      ui.writeDebugLine('foo');
+      expect(ui.output).to.equal(chalk.gray('foo') + EOL);
+    });
+  });
+
+  describe('writeInfoLine', function() {
+    it('writes in the correct chalk', function() {
+      ui.writeInfoLine('foo');
+      expect(ui.output).to.equal(chalk.cyan('foo') + EOL);
+    });
+  });
+
+  describe('writeWarningLine', function() {
+    it('does not write when the test is truthy', function() {
+      ui.writeWarnLine('foo', true);
+      expect(ui.output).to.equal('');
+    });
+
+    it('writes a prepended message when the test is falsy', function() {
+      ui.writeWarnLine('foo', false);
+      expect(ui.output).to.equal(chalk.yellow('WARNING: foo') + EOL);
+    });
+
+    it('writes an un-prepended message if prepend is false', function() {
+      ui.writeWarnLine('foo', false, false);
+      expect(ui.output).to.equal(chalk.yellow('foo') + EOL);
+    });
+  });
+
+  describe('writeDeprecateLine', function() {
+    it('does not write when the test is truthy', function() {
+      ui.writeDeprecateLine('foo', true);
+      expect(ui.output).to.equal('');
+    });
+
+    it('writes a prepended message when the test is falsy', function() {
+      ui.writeDeprecateLine('foo', false);
+      expect(ui.output).to.equal(chalk.yellow('DEPRECATION: foo') + EOL);
+    });
+
+    it('writes an un-prepended message if prepend is false', function() {
+      ui.writeDeprecateLine('foo', false, false);
+      expect(ui.output).to.equal(chalk.yellow('foo') + EOL);
+    });
+  });
+
+  describe('prependLine', function() {
+    it('prepends the data when prepend is undefined', function() {
+      var result = ui.prependLine('foo', 'bar');
+      expect(result).to.equal('foo: bar');
+    });
+
+    it('prepends the data when prepend is true', function() {
+      var result = ui.prependLine('foo', 'bar', true);
+      expect(result).to.equal('foo: bar');
+    });
+
+    it('returns the original data when prepend is falsy (but not undefined)', function() {
+      var result = ui.prependLine('foo', 'bar', false);
+      expect(result).to.equal('bar');
+    });
+  });
+});
diff --git a/tests/unit/ui/write-error-test.js b/tests/unit/ui/write-error-test.js
new file mode 100644
index 000000000..75f4eb99a
--- /dev/null
+++ b/tests/unit/ui/write-error-test.js
@@ -0,0 +1,90 @@
+'use strict';
+
+var expect     = require('chai').expect;
+var writeError = require('../../../lib/ui/write-error');
+var MockUI     = require('../../helpers/mock-ui');
+var BuildError = require('../../helpers/build-error');
+var EOL        = require('os').EOL;
+var chalk      = require('chalk');
+
+describe('writeError', function() {
+  var ui;
+
+  beforeEach(function() {
+    ui = new MockUI();
+  });
+
+  it('no error', function() {
+    writeError(ui);
+
+    expect(ui.output).to.equal('');
+    expect(ui.errors).to.equal('');
+  });
+
+  it('error with message', function() {
+    writeError(ui, new BuildError({
+      message: 'build error'
+    }));
+
+    expect(ui.output).to.equal('');
+    expect(ui.errors).to.equal(chalk.red('build error') + EOL);
+  });
+
+  it('error with stack', function() {
+    writeError(ui, new BuildError({
+      stack: 'the stack'
+    }));
+
+    expect(ui.output).to.equal('');
+    expect(ui.errors).to.equal(chalk.red('Error') + EOL + 'the stack' + EOL);
+  });
+
+  it('error with file', function() {
+    writeError(ui, new BuildError({
+      file: 'the file'
+    }));
+
+    expect(ui.output).to.equal('');
+    expect(ui.errors).to.equal(chalk.red('File: the file') + EOL + chalk.red('Error') + EOL);
+  });
+
+  it('error with filename (as from Uglify)', function() {
+    writeError(ui, new BuildError({
+      filename: 'the file'
+    }));
+
+    expect(ui.output).to.equal('');
+    expect(ui.errors).to.equal(chalk.red('File: the file') + EOL + chalk.red('Error') + EOL);
+  });
+
+  it('error with file + line', function() {
+    writeError(ui, new BuildError({
+      file: 'the file',
+      line: 'the line'
+    }));
+
+    expect(ui.output).to.equal('');
+    expect(ui.errors).to.equal(chalk.red('File: the file (the line)') + EOL + chalk.red('Error') + EOL);
+  });
+
+  it('error with file + col', function() {
+    writeError(ui, new BuildError({
+      file: 'the file',
+      col: 'the col'
+    }));
+
+    expect(ui.output).to.equal('');
+    expect(ui.errors).to.equal(chalk.red('File: the file') + EOL + chalk.red('Error') + EOL);
+  });
+
+  it('error with file + line + col', function() {
+    writeError(ui, new BuildError({
+      file: 'the file',
+      line: 'the line',
+      col:  'the col'
+    }));
+
+    expect(ui.output).to.equal('');
+    expect(ui.errors).to.equal(chalk.red('File: the file (the line:the col)') + EOL + chalk.red('Error') + EOL);
+  });
+});
diff --git a/tests/unit/utilities/process-template-test.js b/tests/unit/utilities/process-template-test.js
deleted file mode 100644
index 09f4d008a..000000000
--- a/tests/unit/utilities/process-template-test.js
+++ /dev/null
@@ -1,17 +0,0 @@
-'use strict';
-
-var expect = require('chai').expect;
-var processTemplate = require('../../../lib/utilities/process-template');
-
-describe('process-template', function() {
-  it('successfully transforms a template', function() {
-
-    expect(processTemplate('hello <%= user %>!', { 'user': 'fred' }))
-      .to.be.equal('hello fred!');
-    expect(processTemplate('<b><%- value %></b>', { 'value': '<script>' }))
-      .to.be.equal('<b>&lt;script&gt;</b>');
-    expect(processTemplate('<% users.forEach(function(user) { %><li><%- user %></li><% }); %>', { 'users': ['fred', 'barney'] }))
-      .to.be.equal('<li>fred</li><li>barney</li>');
-
-  });
-});
diff --git a/tests/unit/utilities/windows-admin-test.js b/tests/unit/utilities/windows-admin-test.js
index 1adf63c37..7b6086618 100644
--- a/tests/unit/utilities/windows-admin-test.js
+++ b/tests/unit/utilities/windows-admin-test.js
@@ -2,7 +2,7 @@
 
 var win = require('../../../lib/utilities/windows-admin');
 var expect = require('chai').expect;
-var MockUI = require('console-ui/mock');
+var MockUI = require('../../helpers/mock-ui');
 
 describe('windows-admin', function() {
   before(function() {