Repository: ember-cli/ember-cli
Pull request #7635 (2018-02-19 01:14:47)

Packaged Bower
This change is related to the [spike](https://github.com/ember-cli/ember-cli/pull/7562) and is backwards compatible. To avoid making changes to `ember-app` in one go, we can move processing/packaging logic (one tree at a time) to `DefaultPackager` while adding tests/documentation and deprecating private undocumented methods.


Patch:
diff --git a/lib/broccoli/default-packager.js b/lib/broccoli/default-packager.js
new file mode 100644
index 000000000..4c4e67d06
--- /dev/null
+++ b/lib/broccoli/default-packager.js
@@ -0,0 +1,55 @@
+'use strict';
+
+const Funnel = require('broccoli-funnel');
+
+const DEFAULT_BOWER_PATH = 'bower_components';
+
+/**
+ * Responsible for packaging Ember.js application.
+ *
+ * @class DefaultPackager
+ * @constructor
+ */
+module.exports = class DefaultPackager {
+  constructor() {
+    this._cachedBower = null;
+  }
+
+  /*
+   * Given an input tree, returns a properly assembled Broccoli tree with bower
+   * components.
+   *
+   * Given a tree:
+   *
+   * ```
+   * ├── ember.js/
+   * ├── pusher/
+   * └── raven-js/
+   * ```
+   *
+   * Returns:
+   *
+   * ```
+   * [bowerDirectory]/
+   * ├── ember.js/
+   * ├── pusher/
+   * └── raven-js/
+   * ```
+   *
+   * @private
+   * @method packageBower
+   * @param {BroccoliTree} tree
+   * @param {String} bowerDirectory Custom path to bower components
+  */
+  packageBower(tree, bowerDirectory) {
+    if (this._cachedBower === null) {
+      this._cachedBower = new Funnel(tree, {
+        srcDir: '/',
+        destDir: bowerDirectory || DEFAULT_BOWER_PATH,
+        annotation: 'Packaged Bower',
+      });
+    }
+
+    return this._cachedBower;
+  }
+};
diff --git a/lib/broccoli/ember-app.js b/lib/broccoli/ember-app.js
index adaade437..daf0b81ed 100644
--- a/lib/broccoli/ember-app.js
+++ b/lib/broccoli/ember-app.js
@@ -47,6 +47,7 @@ const processModulesOnly = require('./babel-process-modules-only');
 const semver = require('semver');
 const Assembler = require('./assembler');
 const strategies = require('./strategies');
+const DefaultPackager = require('./default-packager');
 
 const createVendorJsStrategy = strategies.createVendorJsStrategy;
 const createApplicationJsStrategy = strategies.createApplicationJsStrategy;
@@ -159,6 +160,7 @@ class EmberApp {
     this.legacyTestFilesToAppend = [];
     this.vendorTestStaticStyles = [];
     this._nodeModules = new Map();
+    this._defaultPackager = new DefaultPackager();
 
     this.trees = this.options.trees;
 
@@ -251,6 +253,13 @@ class EmberApp {
     let bowerDirectory = this._resolveLocal(this.bowerDirectory);
     let bowerTree = this.project._watchmanInfo.enabled ? bowerDirectory : new UnwatchedDir(bowerDirectory);
 
+    // Set the flag to make sure:
+    //
+    // - we do not blow up if there is no bower_components folder
+    // - we do not attempt to merge bower and vendor together if they are the
+    //   same tree
+    this._bowerEnabled = this.bowerDirectory !== 'vendor' && existsSync(this.bowerDirectory);
+
     let vendorPath = this._resolveLocal('vendor');
     let vendorTree = existsSync(vendorPath) ? new WatchedDir(vendorPath) : null;
 
@@ -1081,34 +1090,6 @@ class EmberApp {
     });
   }
 
-  /**
-    @private
-    @method _processedBowerTree
-    @return
-  */
-  _processedBowerTree() {
-    if (!this._cachedBowerTree) {
-      // do not attempt to merge bower and vendor together
-      // if they are the same tree
-      if (this.bowerDirectory === 'vendor') {
-        return;
-      }
-
-      // Don't blow up if there is no bower_components folder.
-      if (!existsSync(this.bowerDirectory)) {
-        return;
-      }
-
-      this._cachedBowerTree = new Funnel(this.trees.bower, {
-        srcDir: '/',
-        destDir: `${this.bowerDirectory}/`,
-        annotation: 'Funnel (bower)',
-      });
-    }
-
-    return this._cachedBowerTree;
-  }
-
   _nodeModuleTrees() {
     if (!this._cachedNodeModuleTrees) {
       this._cachedNodeModuleTrees = Array.from(this._nodeModules.values(), module => new Funnel(module.path, {
@@ -1242,12 +1223,13 @@ class EmberApp {
   _processedExternalTree() {
     if (!this._cachedExternalTree) {
       let vendor = this._processedVendorTree();
-      let bower = this._processedBowerTree();
       let addons = this.addonTree();
 
       let trees = [vendor].concat(addons);
-      if (bower) {
-        trees.unshift(bower);
+      if (this._bowerEnabled) {
+        let bower = this._defaultPackager.packageBower(this.trees.bower, this.bowerDirectory);
+
+        trees.push(bower);
       }
 
       trees = this._nodeModuleTrees().concat(trees);


Test patch:
diff --git a/tests/unit/broccoli/default-packager/bower-test.js b/tests/unit/broccoli/default-packager/bower-test.js
new file mode 100644
index 000000000..e19b49740
--- /dev/null
+++ b/tests/unit/broccoli/default-packager/bower-test.js
@@ -0,0 +1,74 @@
+'use strict';
+
+const co = require('co');
+const expect = require('chai').expect;
+const DefaultPackager = require('../../../../lib/broccoli/default-packager');
+const broccoliTestHelper = require('broccoli-test-helper');
+
+const buildOutput = broccoliTestHelper.buildOutput;
+const createTempDir = broccoliTestHelper.createTempDir;
+
+describe('Default Packager: Bower', function() {
+  let input;
+
+  let BOWER_PACKAGES = {
+    'ember.js': {
+      dist: {
+        'ember.js': 'ember',
+      },
+    },
+    pusher: {
+      dist: {
+        'pusher.js': 'pusher',
+      },
+    },
+    'raven-js': {
+      dist: {
+        'raven.js': 'raven',
+      },
+    },
+  };
+
+  before(co.wrap(function *() {
+    input = yield createTempDir();
+
+    input.write(BOWER_PACKAGES);
+  }));
+
+  after(co.wrap(function *() {
+    yield input.dispose();
+  }));
+
+  it('caches packaged bower tree', co.wrap(function *() {
+    let defaultPackager = new DefaultPackager();
+
+    expect(defaultPackager._cachedBower).to.equal(null);
+
+    yield buildOutput(defaultPackager.packageBower(input.path()));
+
+    expect(defaultPackager._cachedBower).to.not.equal(null);
+    expect(defaultPackager._cachedBower._annotation).to.equal('Packaged Bower');
+  }));
+
+  it('packages bower files with default folder', co.wrap(function *() {
+    let defaultPackager = new DefaultPackager();
+
+    let packagedBower = yield buildOutput(defaultPackager.packageBower(input.path()));
+    let output = packagedBower.read();
+
+    expect(output).to.deep.equal({
+      'bower_components': BOWER_PACKAGES,
+    });
+  }));
+
+  it('packages bower files with custom folder', co.wrap(function *() {
+    let defaultPackager = new DefaultPackager();
+
+    let packagedBower = yield buildOutput(defaultPackager.packageBower(input.path(), 'foobar'));
+    let output = packagedBower.read();
+
+    expect(output).to.deep.equal({
+      foobar: BOWER_PACKAGES,
+    });
+  }));
+});