Repository: ember-cli/ember-cli
Pull request #7269 (2017-07-28 21:29:46)

Extract vendor generation into bundler
Follow up on https://github.com/ember-cli/ember-cli/pull/7264.

This change moves `vendor.js` generation into bundler.


Patch:
diff --git a/lib/broccoli/bundler.js b/lib/broccoli/bundler.js
index 4dfd5262b..8e26d2bdd 100644
--- a/lib/broccoli/bundler.js
+++ b/lib/broccoli/bundler.js
@@ -1,6 +1,7 @@
 'use strict';
 
 const concat = require('broccoli-concat');
+const mergeTrees = require('./merge-trees');
 const processModulesOnly = require('./babel-process-modules-only');
 
 module.exports = class Bundler {
@@ -16,9 +17,87 @@ module.exports = class Bundler {
     this.name = options.name;
     this.sourcemaps = options.sourcemaps;
     this.appOutputPath = options.appOutputPath;
+    this.vendorFilePath = options.vendorFilePath;
     this.isBabelAvailable = options.isBabelAvailable;
   }
 
+  /*
+    Creates an array of Broccoli concatenates trees to be included into `vendor.js` file.
+
+    Given an input tree that looks like:
+
+    ```
+    addon-tree-output/
+      ember-ajax/
+      ember-data/
+      ember-engines/
+      ember-resolver/
+      ...
+    bower_components/
+      usertiming/
+      sinonjs/
+      ...
+    the-best-app-ever/
+      components/
+      config/
+      helpers/
+      routes/
+      ...
+    vendor/
+      ...
+      babel-core/
+      ...
+      broccoli-concat/
+      ...
+      ember-cli-template-lint/
+      ...
+    ```
+
+    And a map that looks like:
+
+    {
+      'assets/vendor.js': [
+        'vendor/ember-cli-shims/app-shims.js',
+        'vendor/loader/loader.js',
+        'vendor/ember-resolver/legacy-shims.js',
+        ...
+      ]
+    }
+
+    Produces an array of Broccoli trees to be included into `vendor.js`.
+
+    @private
+    @method getVendorFiles
+    @param {Tree} applicationAndDepsTree Broccoli tree with application and dependencies
+    @param {Object} scriptOutputFiles A map with a key as a file name and list of files to include into `<file-name>.js`
+    @return {Array} Array of trees to be included into resulting `vendor.js` file.
+   */
+  getVendorFiles(applicationAndDepsTree, scriptOutputFiles) {
+    let vendorFiles = [];
+
+    for (let outputFile in scriptOutputFiles) {
+      let isMainVendorFile = outputFile === this.vendorFilePath;
+      let headerFiles = scriptOutputFiles[outputFile];
+      let inputFiles = isMainVendorFile ? ['addon-tree-output/**/*.js'] : [];
+      let footerFiles = isMainVendorFile ? ['vendor/ember-cli/vendor-suffix.js'] : [];
+
+      vendorFiles.push(
+        concat(applicationAndDepsTree, {
+          headerFiles,
+          inputFiles,
+          footerFiles,
+          outputFile,
+          allowNone: true,
+          separator: '\n;',
+          annotation: `Concat: Vendor ${outputFile}`,
+          sourceMapConfig: this.sourcemaps,
+        })
+      );
+    }
+
+    return vendorFiles;
+  }
+
   /*
     Concatenates all javascript Broccoli trees into one, as follows:
 
@@ -57,14 +136,19 @@ module.exports = class Bundler {
     assets/
       the-best-app-ever.js
       the-best-app-ever.map (if sourcemaps are enabled)
+      vendor.js
+      vendor.map (if sourcemaps are enabled)
     ```
 
     @method bundleAppJs
+    @param {Tree} applicationAndDepsTree Broccoli tree with application and dependencies
     @return {Tree} Concatenated tree (application and dependencies).
    */
   bundleJs(applicationAndDepsTree, options) {
     options = options || {};
 
+    let scriptOutputFiles = options.scriptOutputFiles;
+
     let appJs = concat(applicationAndDepsTree, {
       inputFiles: [`${this.name}/**/*.js`],
       headerFiles: [
@@ -76,7 +160,7 @@ module.exports = class Bundler {
         'vendor/ember-cli/app-boot.js',
       ],
       outputFile: this.appOutputPath,
-      annotation: options.appTreeAnnotation,
+      annotation: 'Concat: App',
       sourceMapConfig: this.sourcemaps,
     });
 
@@ -84,6 +168,10 @@ module.exports = class Bundler {
       appJs = processModulesOnly(appJs, 'Babel: Modules for app/');
     }
 
-    return appJs;
+    let vendorFiles = this.getVendorFiles(applicationAndDepsTree, scriptOutputFiles);
+
+    return mergeTrees(vendorFiles.concat(appJs), {
+      annotation: 'TreeMerger (vendor & appJS)',
+    });
   }
 };
diff --git a/lib/broccoli/ember-app.js b/lib/broccoli/ember-app.js
index f38b591d3..6822ce109 100644
--- a/lib/broccoli/ember-app.js
+++ b/lib/broccoli/ember-app.js
@@ -155,6 +155,7 @@ class EmberApp {
       name: this.name,
       sourcemaps: this.options.sourcemaps,
       appOutputPath: this.options.outputPaths.app.js,
+      vendorFilePath: this.options.outputPaths.vendor.js,
       isBabelAvailable: this._addonInstalled('ember-cli-babel'),
     });
   }
@@ -1334,9 +1335,6 @@ class EmberApp {
   javascript() {
     let deprecate = this.project.ui.writeDeprecateLine.bind(this.project.ui);
     let applicationJs = this.appAndDependencies();
-    let appJs = this.bundler.bundleJs(applicationJs, {
-      annotation: 'Concat: App',
-    });
 
     if (this.legacyFilesToAppend.length > 0) {
       deprecate(`Usage of EmberApp.legacyFilesToAppend is deprecated. ` +
@@ -1347,30 +1345,11 @@ class EmberApp {
       });
     }
 
-    this.import('vendor/ember-cli/vendor-prefix.js', { prepend: true });
-
-    let vendorFiles = [];
-    for (let outputFile in this._scriptOutputFiles) {
-      let isMainVendorFile = outputFile === this.options.outputPaths.vendor.js;
-      let headerFiles = this._scriptOutputFiles[outputFile];
-      let inputFiles = isMainVendorFile ? ['addon-tree-output/**/*.js'] : [];
-      let footerFiles = isMainVendorFile ? ['vendor/ember-cli/vendor-suffix.js'] : [];
-
-      vendorFiles.push(
-        this._concatFiles(applicationJs, {
-          headerFiles,
-          inputFiles,
-          footerFiles,
-          outputFile,
-          allowNone: true,
-          separator: '\n;',
-          annotation: `Concat: Vendor ${outputFile}`,
-        })
-      );
-    }
+    let vendorFilePath = this.options.outputPaths.vendor.js;
+    this._scriptOutputFiles[vendorFilePath].unshift('vendor/ember-cli/vendor-prefix.js');
 
-    return mergeTrees(vendorFiles.concat(appJs), {
-      annotation: 'TreeMerger (vendor & appJS)',
+    return this.bundler.bundleJs(applicationJs, {
+      scriptOutputFiles: this._scriptOutputFiles,
     });
   }
 


Test patch:
diff --git a/tests/unit/broccoli/bundler-test.js b/tests/unit/broccoli/bundler-test.js
index 94db374b8..08de54cf6 100644
--- a/tests/unit/broccoli/bundler-test.js
+++ b/tests/unit/broccoli/bundler-test.js
@@ -17,11 +17,15 @@ describe('Bundler', function() {
         name: 'the-best-app-ever',
         sourcemaps: { enabled: true },
         appOutputPath: 'the-best-app-ever.js',
+        vendorFilePath: 'assets/vendor.js',
+        isBabelAvailable: false,
       });
 
       expect(bundler.name).to.equal('the-best-app-ever');
       expect(bundler.sourcemaps).to.deep.equal({ enabled: true });
       expect(bundler.appOutputPath).to.equal('the-best-app-ever.js');
+      expect(bundler.vendorFilePath).to.equal('assets/vendor.js');
+      expect(bundler.isBabelAvailable).to.equal(false);
     });
   });
 
@@ -57,6 +61,15 @@ describe('Bundler', function() {
           },
         },
         'vendor': {
+          'loader': {
+            'loader.js': '',
+          },
+          'ember': {
+            'jquery': {
+              'jquery.js': '',
+            },
+            'ember.debug.js': '',
+          },
           'ember-cli': {
             'app-boot.js': 'app-boot.js',
             'app-config.js': 'app-config.js',
@@ -69,6 +82,12 @@ describe('Bundler', function() {
             'vendor-prefix.js': 'vendor-prefix.js',
             'vendor-suffix.js': 'vendor-suffix.js',
           },
+          'ember-cli-shims': {
+            'app-shims.js': '',
+          },
+          'ember-resolver': {
+            'legacy-shims.js': '',
+          },
         },
       });
     }));
@@ -77,7 +96,7 @@ describe('Bundler', function() {
       input.dispose();
     });
 
-    it('given a tree with `addon` and `vendor` produces a final tree with one javascript file and sourcemap associated with it', co.wrap(function *() {
+    it('given a tree with `addon` and `vendor` produces a final tree with application and vendor javascript and sourcemaps associated with them', co.wrap(function *() {
       let bundler = new Bundler({
         name: 'the-best-app-ever',
         sourcemaps: { enabled: true },
@@ -85,15 +104,67 @@ describe('Bundler', function() {
         appTreeAnnotation: 'concat yo',
       });
 
+      let scriptOutputFiles = {
+        '/assets/vendor.js': [
+          'vendor/ember-cli/vendor-prefix.js',
+          'vendor/loader/loader.js',
+          'vendor/ember/jquery/jquery.js',
+          'vendor/ember/ember.debug.js',
+          'vendor/ember-cli-shims/app-shims.js',
+          'vendor/ember-resolver/legacy-shims.js',
+        ],
+      };
+
       let output = yield buildOutput(bundler.bundleJs(input.path(), {
-        annotation: 'concatinatin\' silly trees',
+        scriptOutputFiles,
       }));
 
       let files = walkSync(output.path(), { directories: false });
       expect(files).to.deep.equal([
+        'assets/vendor.js',
+        'assets/vendor.map',
         'the-best-app-ever.js',
         'the-best-app-ever.map',
       ]);
     }));
+
+    it('vendor files are correctly ordered within concatenated vendor tree', function() {
+      let bundler = new Bundler({
+        name: 'the-best-app-ever',
+        sourcemaps: { enabled: true },
+        appOutputPath: 'the-best-app-ever.js',
+        appTreeAnnotation: 'concat yo',
+        vendorFilePath: '/assets/vendor.js',
+      });
+
+      let vendorFileList = [
+        'vendor/ember-cli/vendor-prefix.js',
+        'files/d.js',
+        'files/a.js',
+        'bower_components/jquery/dist/jquery.js',
+        'bower_components/ember/ember.js',
+        'bower_components/ember-cli-shims/app-shims.js',
+        'files/b.js',
+        'files/c.js',
+      ];
+      let scriptOutputFiles = {
+        '/assets/vendor.js': vendorFileList,
+      };
+
+      let vendorFiles = bundler.getVendorFiles(input.path(), scriptOutputFiles);
+      let vendorTree = vendorFiles[0];
+
+      expect(vendorTree.headerFiles).to.deep.equal(vendorFileList);
+      expect(vendorTree.inputFiles).to.deep.equal([
+        'addon-tree-output/**/*.js',
+      ]);
+      expect(vendorTree.footerFiles).to.deep.equal([
+        'vendor/ember-cli/vendor-suffix.js',
+      ]);
+      expect(vendorTree.outputFile).to.equal('/assets/vendor.js');
+      expect(vendorTree.separator).to.equal('\n;');
+      expect(vendorTree.allowNone).to.equal(true);
+      expect(vendorTree._annotation).to.equal('Concat: Vendor /assets/vendor.js');
+    });
   });
 });
diff --git a/tests/unit/broccoli/ember-app-test.js b/tests/unit/broccoli/ember-app-test.js
index ad64fde64..885397550 100644
--- a/tests/unit/broccoli/ember-app-test.js
+++ b/tests/unit/broccoli/ember-app-test.js
@@ -1050,21 +1050,6 @@ describe('EmberApp', function() {
     describe('concat order', function() {
       let count = 0;
       let args = [];
-      let concatAppJsTree = {
-        annotation: "Concat: App",
-        footerFiles: [
-          "vendor/ember-cli/app-suffix.js",
-          "vendor/ember-cli/app-config.js",
-          "vendor/ember-cli/app-boot.js",
-        ],
-        headerFiles: [
-          "vendor/ember-cli/app-prefix.js",
-        ],
-        inputFiles: [
-          "test-project/**/*.js",
-        ],
-        outputFile: "/assets/test-project.js",
-      };
 
       beforeEach(function() {
         count = 0;
@@ -1078,12 +1063,6 @@ describe('EmberApp', function() {
           return tree;
         };
 
-        app.bundler.bundleJs = function(tree) {
-          count++;
-          args.push(concatAppJsTree);
-          return tree;
-        };
-
         app.appAndDependencies = function() {
           return 'app-and-dependencies-tree';
         };
@@ -1141,42 +1120,6 @@ describe('EmberApp', function() {
         });
       });
 
-      it('correctly orders concats from app.javascript()', function() {
-        app.import('files/b.js');
-        app.import('files/c.js');
-        app.import('files/a.js');
-        app.import('files/a.js', { prepend: true }); // Should end up second.
-        app.import('files/d.js');
-        app.import('files/d.js', { prepend: true }); // Should end up first.
-        app.import('files/d.js');
-
-        app.javascript(); // run
-
-        expect(count).to.eql(2);
-        // should be unrelated files
-        expect(args[0]).to.deep.eql(concatAppJsTree);
-
-        // should be: a,b,c,d in output
-        expect(args[1]).to.deep.eql({
-          annotation: "Concat: Vendor /assets/vendor.js",
-          allowNone: true,
-          headerFiles: [
-            "vendor/ember-cli/vendor-prefix.js",
-            "files/d.js",
-            "files/a.js",
-            "bower_components/jquery/dist/jquery.js",
-            "bower_components/ember/ember.js",
-            "bower_components/ember-cli-shims/app-shims.js",
-            "files/b.js",
-            "files/c.js",
-          ],
-          inputFiles: ['addon-tree-output/**/*.js'],
-          footerFiles: ['vendor/ember-cli/vendor-suffix.js'],
-          outputFile: "/assets/vendor.js",
-          separator: '\n;',
-        });
-      });
-
       it('correctly orders concats from app.testFiles()', function() {
         app.import('files/b.js', { type: 'test' });
         app.import('files/c.js', { type: 'test' });