lib/broccoli/default-packager.js

'use strict';

const p = require('ember-cli-preprocess-registry/preprocessors');
const path = require('path');
const concat = require('broccoli-concat');
const Funnel = require('broccoli-funnel');
const BroccoliDebug = require('broccoli-debug');
const mergeTrees = require('./merge-trees');
const ConfigLoader = require('broccoli-config-loader');
const UnwatchedDir = require('broccoli-source').UnwatchedDir;
const ConfigReplace = require('broccoli-config-replace');
const emberAppUtils = require('../utilities/ember-app-utils');
const funnelReducer = require('broccoli-funnel-reducer');
const addonProcessTree = require('../utilities/addon-process-tree');

const preprocessCss = p.preprocessCss;
const preprocessJs = p.preprocessJs;
const preprocessTemplates = p.preprocessTemplates;
const preprocessMinifyCss = p.preprocessMinifyCss;

const DEFAULT_VENDOR_PATH = 'vendor';
const EMBER_CLI_INTERNAL_FILES_PATH = '/vendor/ember-cli/';
const EMBER_CLI_FILES = [
  'app-boot.js',
  'app-config.js',
  'app-prefix.js',
  'app-suffix.js',
  'test-support-prefix.js',
  'test-support-suffix.js',
  'tests-prefix.js',
  'tests-suffix.js',
  'vendor-prefix.js',
  'vendor-suffix.js',
];

const configReplacePatterns = emberAppUtils.configReplacePatterns;

function callAddonsPreprocessTreeHook(project, type, tree) {
  return addonProcessTree(project, 'preprocessTree', type, tree);
}

function callAddonsPostprocessTreeHook(project, type, tree) {
  return addonProcessTree(project, 'postprocessTree', type, tree);
}

/*
  Creates an object with lists of files to be concatenated into `vendor.js` file.

  Given 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 object that looks like:

  ```
  {
    headerFiles: [
     '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'
    ],
    inputFiles: [
      'addon-tree-output/**\/*.js'
    ],
    footerFiles: [
      'vendor/ember-cli/vendor-suffix.js'
    ],
    annotation: 'Vendor JS'
  }
  ```

  @private
  @method getVendorFiles
  @param {Object} files A list of files to include into `<file-name>.js`
  @param {Boolean} isMainVendorFile Boolean flag to indicate if we are dealing with `vendor.js` file
  @return {Object} An object with lists of files to be concatenated into `vendor.js` file.
 */
function getVendorFiles(files, isMainVendorFile) {
  return {
    headerFiles: files,
    inputFiles: isMainVendorFile ? ['addon-tree-output/**/*.js'] : [],
    footerFiles: isMainVendorFile ? ['vendor/ember-cli/vendor-suffix.js'] : [],
  };
}

/**
 * Responsible for packaging Ember.js application.
 *
 * @class DefaultPackager
 * @constructor
 */
module.exports = class DefaultPackager {
  constructor(options) {
    this._cachedTests = null;
    this._cachedVendor = null;
    this._cachedPublic = null;
    this._cachedConfig = null;
    this._cachedJavascript = null;
    this._cachedProcessedIndex = null;
    this._cachedTransformedTree = null;
    this._cachedProcessedStyles = null;
    this._cachedProcessedTemplates = null;
    this._cachedProcessedJavascript = null;
    this._cachedEmberCliInternalTree = null;
    this._cachedProcessedAdditionalAssets = null;
    this._cachedProcessedAppAndDependencies = null;

    this.options = options || {};

    this._debugTree = BroccoliDebug.buildDebugCallback('default-packager');

    this.env = this.options.env;
    this.name = this.options.name;
    this.autoRun = this.options.autoRun;
    this.project = this.options.project;
    this.registry = this.options.registry;
    this.sourcemaps = this.options.sourcemaps;
    this.minifyCSS = this.options.minifyCSS;
    this.distPaths = this.options.distPaths;
    this.areTestsEnabled = this.options.areTestsEnabled;
    this.styleOutputFiles = this.options.styleOutputFiles;
    this.scriptOutputFiles = this.options.scriptOutputFiles;
    this.storeConfigInMeta = this.options.storeConfigInMeta;
    this.customTransformsMap = this.options.customTransformsMap;
    this.additionalAssetPaths = this.options.additionalAssetPaths;
    this.vendorTestStaticStyles = this.options.vendorTestStaticStyles;
    this.legacyTestFilesToAppend = this.options.legacyTestFilesToAppend;
  }

  /*
   * Replaces variables in `index.html` file with values from
   * `config/environment.js` and returns a single tree that contains
   * `index.html` file with populated values.
   *
   * Input tree:
   *
   * ```
   * /
   * ├── addon-tree-output/
   * ├── the-best-app-ever/
   * └── vendor/
   * ```
   *
   * Returns:
   *
   * ```
   * /
   * └── index.html
   * ```
   *
   * @private
   * @method processIndex
   * @param {BroccoliTree}
   * @return {BroccoliTree}
   */
  processIndex(tree) {
    if (this._cachedProcessedIndex === null) {
      let indexFilePath = this.distPaths.appHtmlFile;

      let index = new Funnel(tree, {
        allowEmtpy: true,
        include: [`${this.name}/index.html`],
        getDestinationPath: () => indexFilePath,
        annotation: 'Classic: index.html',
      });

      let patterns = configReplacePatterns({
        addons: this.project.addons,
        autoRun: this.autoRun,
        storeConfigInMeta: this.storeConfigInMeta,
      });

      this._cachedProcessedIndex = new ConfigReplace(index, this.packageConfig(), {
        configPath: path.join(this.name, 'config', 'environments', `${this.env}.json`),
        files: [indexFilePath],
        patterns,
      });
    }

    return this._cachedProcessedIndex;
  }

  /*
   * Combines compiled javascript, external files (node modules),
   * vendor files and processed configuration (based on the
   * environment) into a single tree.
   *
   * Input tree:
   *
   * ```
   * /
   * ├── addon-tree-output/
   * ├── the-best-app-ever/
   * └── vendor/
   * ```
   *
   * Changes are made "inline" so the output tree has the same structure.
   *
   * @private
   * @method processAppAndDependencies
   * @param {BroccoliTree}
   * @return {BroccoliTree}
   */
  processAppAndDependencies(allTrees) {
    if (this._cachedProcessedAppAndDependencies === null) {
      let config = this.packageConfig();
      let internal = this.packageEmberCliInternalFiles();
      let appContentsWithCompiledTemplates = this._debugTree(
        this.processTemplates(allTrees),
        'app-and-deps:post-templates'
      );

      let trees = [allTrees, appContentsWithCompiledTemplates].filter(Boolean);

      let mergedTree = this._debugTree(
        mergeTrees(trees, {
          annotation: 'TreeMerger (preprocessedApp & templates)',
          overwrite: true,
        }),
        'app-and-deps:merged'
      );

      let external = this.applyCustomTransforms(allTrees);
      let postprocessedApp = this.processJavascript(mergedTree);

      let sourceTrees = [external, postprocessedApp, config, internal];

      this._cachedProcessedAppAndDependencies = this._debugTree(
        mergeTrees(sourceTrees, {
          overwrite: true,
          annotation: 'Processed Application and Dependencies',
        }),
        'app-and-deps:final'
      );
    }

    return this._cachedProcessedAppAndDependencies;
  }

  /*
   * Adds additional assets to the results tree, given the following list:
   *
   * ```
   * [{
   *   src: 'vendor/font-awesome/fonts',
   *   file: 'FontAwesome.otf',
   *   dest: 'fonts'
   * }]
   * ```
   *
   * where `src` is a source path, `file` is a file name, and `dest` is a new
   * destination.
   *
   * @private
   * @method importAdditionalAssets
   * @param {BroccoliTree}
   * @return {BroccoliTree}
   */
  importAdditionalAssets(tree) {
    if (this._cachedProcessedAdditionalAssets === null) {
      let otherAssetTrees = funnelReducer(this.additionalAssetPaths).map((options) => {
        let files = options.include.join(',');
        options.annotation = `${options.srcDir}/{${files}} => ${options.destDir}/{${files}}`;

        return new Funnel(tree, options);
      });

      this._cachedProcessedAdditionalAssets = mergeTrees(otherAssetTrees, {
        annotation: 'Processed Additional Assets',
      });
    }

    return this._cachedProcessedAdditionalAssets;
  }

  /*
   * Runs all registered transformations on the passed in tree and returns the
   * result.
   *
   * Passed-in tree:
   *
   * ```
   * /
   * ├── addon-tree-output/
   * └── vendor/
   * ```
   *
   * `customTransformsMap` has information about files that needed to be
   * transformed and the actual transformation functions that are executed.
   *
   * @private
   * @method applyCustomTransforms
   * @param {BroccoliTree} External (vendor) tree
   * @return {BroccoliTree}
   */
  applyCustomTransforms(externalTree) {
    if (this._cachedTransformedTree === null) {
      this._cachedTransformedTree = externalTree;

      for (let customTransformEntry of this.customTransformsMap) {
        let transformName = customTransformEntry[0];
        let transformConfig = customTransformEntry[1];

        let transformTree = new Funnel(this._cachedTransformedTree, {
          files: transformConfig.files,
          annotation: `Funnel (custom transform: ${transformName})`,
        });

        this._cachedTransformedTree = mergeTrees(
          [this._cachedTransformedTree, transformConfig.callback(transformTree, transformConfig.options)],
          {
            annotation: `TreeMerger (custom transform: ${transformName})`,
            overwrite: true,
          }
        );
      }
    }

    return this._cachedTransformedTree;
  }

  /*
   * Returns a single tree with `ember-cli` internal files with the following
   * structure:
   *
   * ```
   * vendor/
   * └── ember-cli
   *     ├── app-boot.js
   *     ├── app-config.js
   *     ├── app-prefix.js
   *     ├── app-suffix.js
   *     ├── test-support-suffix.js
   *     ├── test-support-prefix.js
   *     ├── tests-prefix.js
   *     ├── tests-suffix.js
   *     ├── vendor-prefix.js
   *     └── vendor-suffix.js
   * ```
   *
   * Note, that the contents of these files is being matched against several
   * internal `ember-cli` variables, such as:
   *
   * + `{{MODULE_PREFIX}}`
   * + different types of `{{content-for}}` (`{{content-for 'app-boot'}}`)
   *
   * @private
   * @method packageEmberCliInternalFiles
   * @return {BroccoliTree}
   */
  packageEmberCliInternalFiles() {
    if (this._cachedEmberCliInternalTree === null) {
      let patterns = configReplacePatterns({
        addons: this.project.addons,
        autoRun: this.autoRun,
        storeConfigInMeta: this.storeConfigInMeta,
      });

      let configTree = this.packageConfig();
      let configPath = path.join(this.name, 'config', 'environments', `${this.env}.json`);

      let emberCLITree = new ConfigReplace(new UnwatchedDir(__dirname), configTree, {
        configPath,
        files: EMBER_CLI_FILES,
        patterns,
      });

      this._cachedEmberCliInternalTree = new Funnel(emberCLITree, {
        files: EMBER_CLI_FILES,
        destDir: EMBER_CLI_INTERNAL_FILES_PATH,
        annotation: 'Packaged Ember CLI Internal Files',
      });
    }

    return this._cachedEmberCliInternalTree;
  }

  /*
   * Runs pre/post-processors hooks on the template files and returns a single
   * tree with the processed templates.
   *
   * Given a tree:
   *
   * ```
   * /
   * ├── addon-tree-output/
   * ├── the-best-app-ever/
   * └── vendor/
   * ```
   *
   * Returns:
   *
   * ```
   * the-best-app-ever/
   * └── templates
   *     ├── application.js
   *     ├── error.js
   *     ├── index.js
   *     └── loading.js
   * ```
   *
   * @private
   * @method processTemplates
   * @param {BroccoliTree} tree
   * @return {BroccoliTree}
   */
  processTemplates(inputTree) {
    if (this._cachedProcessedTemplates === null) {
      let appFiles = new Funnel(inputTree, {
        srcDir: `${this.name}/`,
        destDir: `${this.name}/`,
        annotation: 'processTemplates: app files',
      });

      let mergedTemplates = [appFiles];

      mergedTemplates = mergeTrees(mergedTemplates, {
        overwrite: true,
        annotation: 'Templates',
      });
      let preprocessedTemplatesFromAddons = callAddonsPreprocessTreeHook(this.project, 'template', mergedTemplates);

      this._cachedProcessedTemplates = callAddonsPostprocessTreeHook(
        this.project,
        'template',
        preprocessTemplates(preprocessedTemplatesFromAddons, {
          registry: this.registry,
          treeType: 'templates',
        })
      );
    }

    return this._cachedProcessedTemplates;
  }

  /*
   * Runs pre/post-processors hooks on the javascript files and returns a single
   * tree with the processed javascript.
   *
   * Given a tree:
   *
   * ```
   * /
   * ├── addon-tree-output/
   * ├── the-best-app-ever/
   * └── vendor/
   * ```
   *
   * Returns:
   *
   * ```
   * the-best-app-ever/
   * ├── adapters
   * │   └── application.js
   * ├── app.js
   * ├── components
   * ├── controllers
   * ├── helpers
   * │   ├── and.js
   * │   ├── app-version.js
   * │   ├── await.js
   * │   ├── camelize.js
   * │   ├── cancel-all.js
   * │   ├── dasherize.js
   * │   ├── dec.js
   * │   ├── drop.js
   * │   └── eq.js
   * ...
   * ```
   *
   * @private
   * @method processJavascript
   * @param {BroccoliTree} tree
   * @return {BroccoliTree}
   */
  processJavascript(tree) {
    if (this._cachedProcessedJavascript === null) {
      let javascript = new Funnel(tree, {
        srcDir: this.name,
        destDir: this.name,
        annotation: '',
      });
      let app = callAddonsPreprocessTreeHook(this.project, 'js', javascript);

      let preprocessedApp = preprocessJs(app, '/', this.name, {
        registry: this.registry,
        treeType: 'app',
      });

      this._cachedProcessedJavascript = callAddonsPostprocessTreeHook(this.project, 'js', preprocessedApp);
    }

    return this._cachedProcessedJavascript;
  }

  /*
   * Compiles application css files, runs pre/post-processors hooks on the them,
   * concatenates them into one application and vendor files and returns a
   * single tree.
   *
   * Given an input tree that looks like:
   *
   * ```
   * addon-tree-output/
   *   ...
   * the-best-app-ever/
   *   styles/
   *   ...
   * vendor/
   *   font-awesome/
   *   ...
   * ```
   *
   * Returns:
   *
   * ```
   * assets/
   *   the-best-app-ever.css
   *   vendor.css
   * ```
   *
   * @private
   * @method packageStyles
   * @return {BroccoliTree}
   */
  packageStyles(tree) {
    if (this._cachedProcessedStyles === null) {
      let cssMinificationEnabled = this.minifyCSS.enabled;
      let options = {
        outputPaths: this.distPaths.appCssFile,
        registry: this.registry,
        minifyCSS: this.minifyCSS.options,
        treeType: 'styles',
      };

      let stylesAndVendor = callAddonsPreprocessTreeHook(this.project, 'css', tree);
      let preprocessedStyles = preprocessCss(stylesAndVendor, '/app/styles', '/assets', options);

      let vendorStyles = [];
      for (let outputFile in this.styleOutputFiles) {
        let isMainVendorFile = outputFile === this.distPaths.vendorCssFile;
        let headerFiles = this.styleOutputFiles[outputFile];
        let inputFiles = isMainVendorFile ? ['addon-tree-output/**/__COMPILED_STYLES__/**/*.css'] : [];

        vendorStyles.push(
          concat(stylesAndVendor, {
            headerFiles,
            inputFiles,
            outputFile,
            allowNone: true,
            annotation: `Concat: Vendor Styles${outputFile}`,
          })
        );
      }

      vendorStyles = mergeTrees(vendorStyles, {
        annotation: 'TreeMerger (vendorStyles)',
        overwrite: true,
      });

      if (cssMinificationEnabled === true) {
        options.minifyCSS.registry = options.registry;
        preprocessedStyles = preprocessMinifyCss(preprocessedStyles, options.minifyCSS);
        vendorStyles = preprocessMinifyCss(vendorStyles, options.minifyCSS);
      }

      this._cachedProcessedStyles = callAddonsPostprocessTreeHook(
        this.project,
        'css',
        mergeTrees([preprocessedStyles, vendorStyles], {
          annotation: 'Packaged Styles',
        })
      );
    }

    return this._cachedProcessedStyles;
  }

  /*
   * Given an input tree, returns a properly assembled Broccoli tree with vendor
   * files.
   *
   * Given a tree:
   *
   * ```
   * ├── babel-polyfill/
   * ├── ember-cli-shims/
   * ├── ember-load-initializers/
   * ├── ember-qunit/
   * ├── ember-resolver/
   * ├── sinon/
   * └── tether/
   * ```
   *
   * Returns:
   *
   * ```
   * vendor/
   * ├── babel-polyfill/
   * ├── ember-cli-shims/
   * ├── ember-load-initializers/
   * ├── ember-qunit/
   * ├── ember-resolver/
   * ├── sinon/
   * └── tether/
   * ```
   *
   * @private
   * @method packageVendor
   * @param {BroccoliTree} tree
   */
  packageVendor(tree) {
    if (this._cachedVendor === null) {
      this._cachedVendor = new Funnel(tree, {
        destDir: DEFAULT_VENDOR_PATH,
        annotation: 'Packaged Vendor',
      });
    }

    return this._cachedVendor;
  }

  /*
   * Given an input tree, returns a properly assembled Broccoli tree with tests
   * files.
   *
   * Given a tree:
   *
   * ```
   * addon-tree-output/
   * the-best-app-ever/
   * tests/
   * ├── acceptance/
   * ├── helpers/
   * ├── index.html
   * ├── integration/
   * ├── test-helper.js
   * └── unit/
   * ```
   *
   * Returns:
   *
   * ```
   * [name]/
   * └── tests
   *     ├── acceptance/
   *     ├── helpers/
   *     ├── index.html
   *     ├── integration/
   *     ├── test-helper.js
   *     └── unit/
   * ```
   *
   * @private
   * @method processTests
   * @param {BroccoliTree} tree
   */
  processTests(tree) {
    if (this._cachedTests === null) {
      let addonTestSupportTree = new Funnel(tree, {
        srcDir: 'tests/addon-test-support',
        destDir: 'addon-test-support',
      });

      let testTree = new Funnel(tree, {
        srcDir: 'tests',
        exclude: ['addon-test-support/**/*'],
      });

      let treeToCompile = new Funnel(testTree, {
        destDir: `${this.name}/tests`,
        annotation: 'Tests To Process',
      });

      treeToCompile = callAddonsPreprocessTreeHook(this.project, 'test', treeToCompile);

      const inputPath = '/tests';
      let preprocessedTests = preprocessJs(treeToCompile, inputPath, this.name, {
        registry: this.registry,
        treeType: 'test',
      });

      let mergedTestTrees = mergeTrees([addonTestSupportTree, preprocessedTests], {
        overwrite: true,
        annotation: 'Packaged Tests',
      });

      this._cachedTests = callAddonsPostprocessTreeHook(this.project, 'test', mergedTestTrees);
    }

    return this._cachedTests;
  }

  /*
   * Concatenates all test files into one, as follows:
   *
   * Given an input tree that looks like:
   *
   * ```
   * addon-tree-output/
   * the-best-app-ever/
   * tests/
   * ├── acceptance/
   * ├── helpers/
   * ├── index.html
   * ├── integration/
   * ├── test-helper.js
   * └── unit/
   * vendor/
   * ```
   *
   * Returns:
   *
   * ```
   * assets/
   *   tests.js
   *   test.map (if sourcemaps are enabled)
   *   test-support.js
   *   test-support.map (if sourcemaps are enabled)
   *   test-support.css
   * ```
   *
   * @private
   * @method packageTests
   * @param {BroccoliTree}
   * @return {BroccoliTree}
   */
  packageTests(tree) {
    let coreTestTree = this.processTests(tree);

    let testIndex = this.processTestIndex(tree);
    let appTestTree = this.packageApplicationTests(coreTestTree);
    let testFilesTree = this.packageTestFiles(tree, coreTestTree);

    return mergeTrees([testIndex, appTestTree, testFilesTree], {
      annotation: 'Packaged Tests',
    });
  }

  /*
   * Replaces variables in `tests/index.html` file with values from
   * `config/environment.js` and returns a single tree that contains
   * `index.html` file with populated values.
   *
   * Input tree:
   *
   * ```
   * /
   * ├── addon-tree-output/
   * ├── the-best-app-ever/
   * ├── tests/
   * └── vendor/
   * ```
   *
   * Returns:
   *
   * ```
   * └── tests
   *     └── index.html/
   * ```
   *
   * @private
   * @method processTestIndex
   * @param {BroccoliTree}
   * @return {BroccoliTree}
   */
  processTestIndex(tree) {
    let index = new Funnel(tree, {
      srcDir: '/tests',
      files: ['index.html'],
      destDir: '/tests',
      annotation: 'Funnel (test index)',
    });

    let patterns = configReplacePatterns({
      addons: this.project.addons,
      autoRun: this.autoRun,
      storeConfigInMeta: this.storeConfigInMeta,
    });

    let configPath = path.join(this.name, 'config', 'environments', 'test.json');

    return new ConfigReplace(index, this.packageConfig(), {
      configPath,
      files: ['tests/index.html'],
      env: 'test',
      patterns,
    });
  }

  /*
   * Wraps application configuration into AMD module:
   *
   * ```javascript
   * define('the-best-app-ever/config/environment', [], function() {
   *   // read the meta tag that contains escaped configuration from
   *   // `index.html` and return as an object
   * });
   * ```
   *
   * Given a tree:
   *
   * ```
   * environments/
   * ├── development.json
   * └── test.json
   * ```
   *
   * Returns:
   *
   * ```
   * └── vendor
   *     └── ember-cli
   *         └── app-config.js
   * ```
   * @private
   * @method packageTestApplicationConfig
   */
  packageTestApplicationConfig() {
    let files = ['app-config.js'];
    let patterns = configReplacePatterns({
      addons: this.project.addons,
      autoRun: this.autoRun,
      storeConfigInMeta: this.storeConfigInMeta,
    });

    let configPath = path.join(this.name, 'config', 'environments', `test.json`);
    let emberCLITree = new ConfigReplace(new UnwatchedDir(__dirname), this.packageConfig(), {
      configPath,
      files,
      patterns,
    });

    return new Funnel(emberCLITree, {
      files,
      srcDir: '/',
      destDir: '/vendor/ember-cli/',
      annotation: 'Funnel (test-app-config-tree)',
    });
  }

  /*
   * Concatenates all application test files into one, as follows:
   *
   * Given an input tree that looks like:
   *
   * ```
   * addon-tree-output/
   * the-best-app-ever/
   * tests/
   * ├── acceptance/
   * ├── helpers/
   * ├── index.html
   * ├── integration/
   * ├── test-helper.js
   * └── unit/
   * vendor/
   * ```
   *
   * Returns:
   *
   * ```
   * assets/
   *   tests.js
   *   test.map (if sourcemaps are enabled)
   * ```
   *
   * @private
   * @method packageApplicationTests
   * @param {BroccoliTree}
   * @return {BroccoliTree}
   */
  packageApplicationTests(tree) {
    let appTestTrees = []
      .concat(this.packageEmberCliInternalFiles(), this.packageTestApplicationConfig(), tree)
      .filter(Boolean);

    appTestTrees = mergeTrees(appTestTrees, {
      overwrite: true,
      annotation: 'TreeMerger (appTestTrees)',
    });

    return concat(appTestTrees, {
      inputFiles: ['**/tests/**/*.js'],
      headerFiles: ['vendor/ember-cli/tests-prefix.js'],
      footerFiles: ['vendor/ember-cli/app-config.js', 'vendor/ember-cli/tests-suffix.js'],
      outputFile: this.distPaths.testJsFile,
      annotation: 'Concat: App Tests',
      sourceMapConfig: this.sourcemaps,
    });
  }

  /*
   * Concatenates all test support files into one, as follows:
   *
   * Given an input tree that looks like:
   *
   * ```
   * addon-tree-output/
   * the-best-app-ever/
   * tests/
   * ├── acceptance/
   * ├── helpers/
   * ├── index.html
   * ├── integration/
   * ├── test-helper.js
   * └── unit/
   * vendor/
   * ```
   *
   * Returns:
   *
   * ```
   * assets/
   *   test-support.js
   *   test-support.map (if sourcemaps are enabled)
   *   test-support.css
   * ```
   *
   * @private
   * @method packageTestFiles
   * @return {BroccoliTree}
   */
  packageTestFiles(tree, coreTestTree) {
    let testSupportPath = this.distPaths.testSupportJsFile;

    testSupportPath = testSupportPath.testSupport || testSupportPath;

    let emberCLITree = this.packageEmberCliInternalFiles();

    let headerFiles = [].concat('vendor/ember-cli/test-support-prefix.js', this.legacyTestFilesToAppend);

    let inputFiles = ['addon-test-support/**/*.js'];

    let footerFiles = ['vendor/ember-cli/test-support-suffix.js'];

    let external = this.applyCustomTransforms(tree);

    let baseMergedTree = mergeTrees([emberCLITree, tree, external, coreTestTree], {
      overwrite: true,
    });
    let testJs = concat(baseMergedTree, {
      headerFiles,
      inputFiles,
      footerFiles,
      outputFile: testSupportPath,
      annotation: 'Concat: Test Support JS',
      allowNone: true,
      sourceMapConfig: this.sourcemaps,
    });

    let testemPath = path.join(__dirname, 'testem');
    testemPath = path.dirname(testemPath);

    let testemTree = new Funnel(new UnwatchedDir(testemPath), {
      files: ['testem.js'],
      annotation: 'Funnel (testem)',
    });

    let sourceTrees = [testemTree, testJs];

    if (this.vendorTestStaticStyles.length > 0) {
      sourceTrees.push(
        concat(tree, {
          headerFiles: this.vendorTestStaticStyles,
          outputFile: this.distPaths.testSupportCssFile,
          annotation: 'Concat: Test Support CSS',
          sourceMapConfig: this.sourcemaps,
        })
      );
    }

    return mergeTrees(sourceTrees, {
      overwrite: true,
      annotation: 'TreeMerger (testFiles)',
    });
  }

  /*
   * Returns a flattened input tree.
   *
   * Given a tree:
   *
   * ```
   * public
   * ├── crossdomain.xml
   * ├── ember-fetch
   * ├── favicon.ico
   * ├── images
   * └── robots.txt
   * ```
   *
   * Returns:
   *
   * ```
   * ├── crossdomain.xml
   * ├── ember-fetch
   * ├── favicon.ico
   * ├── images
   * └── robots.txt
   * ```
   *
   * @private
   * @method packagePublic
   * @param {BroccoliTree} tree
   * @return {BroccoliTree}
   */
  packagePublic(tree) {
    if (this._cachedPublic === null) {
      this._cachedPublic = new Funnel(tree, {
        srcDir: 'public',
        destDir: '.',
      });
    }

    return this._cachedPublic;
  }

  /*
   * Given an input tree, returns a properly assembled Broccoli tree with
   * configuration files.
   *
   * Given a tree:
   *
   * ```
   * environments/
   * ├── development.json
   * └── test.json
   * ```
   *
   * Returns:
   *
   * ```
   * └── [name]
   *     └── config
   *         └── environments
   *             ├── development.json
   *             └── test.json
   * ```
   * @private
   * @method packageConfig
   */
  packageConfig() {
    let env = this.env;
    let name = this.name;
    let project = this.project;
    let configPath = this.project.configPath();

    if (this._cachedConfig === null) {
      let configTree = new ConfigLoader(path.dirname(configPath), {
        env,
        tests: this.areTestsEnabled || false,
        project,
      });

      this._cachedConfig = new Funnel(configTree, {
        destDir: `${name}/config`,
        annotation: 'Packaged Config',
      });
    }

    return this._cachedConfig;
  }

  /*
   * Concatenates all javascript Broccoli trees into one, as follows:
   *
   * Given an input tree that looks like:
   *
   * ```
   * addon-tree-output/
   *   ember-ajax/
   *   ember-data/
   *   ember-engines/
   *   ember-resolver/
   *   ...
   * the-best-app-ever/
   *   components/
   *   config/
   *   helpers/
   *   routes/
   *   ...
   * vendor/
   *   ...
   *   babel-core/
   *   ...
   *   broccoli-concat/
   *   ...
   *   ember-cli-template-lint/
   *   ...
   * ```
   *
   * Returns:
   *
   * ```
   * assets/
   *   the-best-app-ever.js
   *   the-best-app-ever.map (if sourcemaps are enabled)
   *   vendor.js
   *   vendor.map (if sourcemaps are enabled)
   * ```
   *
   * @private
   * @method packageJavascript
   * @return {BroccoliTree}
   */
  packageJavascript(tree) {
    if (this._cachedJavascript === null) {
      let applicationJs = this.processAppAndDependencies(tree);

      let vendorFilePath = this.distPaths.vendorJsFile;
      this.scriptOutputFiles[vendorFilePath].unshift('vendor/ember-cli/vendor-prefix.js');

      let appJs = this.packageApplicationJs(applicationJs);
      let vendorJs = this.packageVendorJs(applicationJs);

      this._cachedJavascript = mergeTrees([appJs, vendorJs], {
        overwrite: true,
        annotation: 'Packaged Javascript',
      });
    }

    return this._cachedJavascript;
  }

  /*
   * Concatenates all application's javascript Broccoli trees into one, as follows:
   *
   * Given an input tree that looks like:
   *
   * ```
   * addon-tree-output/
   *   ember-ajax/
   *   ember-data/
   *   ember-engines/
   *   ember-resolver/
   *   ...
   * the-best-app-ever/
   *   components/
   *   config/
   *   helpers/
   *   routes/
   *   ...
   * vendor/
   *   ...
   *   babel-core/
   *   ...
   *   broccoli-concat/
   *   ...
   *   ember-cli-template-lint/
   *   ...
   * ```
   *
   * Returns:
   *
   * ```
   * assets/
   *   the-best-app-ever.js
   *   the-best-app-ever.map (if sourcemaps are enabled)
   * ```
   *
   * @private
   * @method packageApplicationJs
   * @return {BroccoliTree}
   */
  packageApplicationJs(tree) {
    let inputFiles = [`${this.name}/**/*.js`];
    let headerFiles = ['vendor/ember-cli/app-prefix.js'];
    let footerFiles = [
      'vendor/ember-cli/app-suffix.js',
      'vendor/ember-cli/app-config.js',
      'vendor/ember-cli/app-boot.js',
    ];

    return concat(tree, {
      inputFiles,
      headerFiles,
      footerFiles,
      outputFile: this.distPaths.appJsFile,
      annotation: 'Packaged Application Javascript',
      separator: '\n;',
      sourceMapConfig: this.sourcemaps,
    });
  }

  /*
   * Concatenates all application's vendor javascript Broccoli trees into one, as follows:
   *
   * Given an input tree that looks like:
   * ```
   * addon-tree-output/
   *   ember-ajax/
   *   ember-data/
   *   ember-engines/
   *   ember-resolver/
   *   ...
   * the-best-app-ever/
   *   components/
   *   config/
   *   helpers/
   *   routes/
   *   ...
   * vendor/
   *   ...
   *   babel-core/
   *   ...
   *   broccoli-concat/
   *   ...
   *   ember-cli-template-lint/
   *   ...
   * ```
   *
   * Returns:
   *
   * ```
   * assets/
   *   vendor.js
   *   vendor.map (if sourcemaps are enabled)
   * ```
   *
   * @method packageVendorJs
   * @param {BroccoliTree} tree
   * @return {BroccoliTree}
   */
  packageVendorJs(tree) {
    let importPaths = Object.keys(this.scriptOutputFiles);

    // iterate over the keys and concat files
    // to support scenarios like
    // app.import('vendor/foobar.js', { outputFile: 'assets/baz.js' });
    let vendorTrees = importPaths.map((importPath) => {
      let files = this.scriptOutputFiles[importPath];
      let isMainVendorFile = importPath === this.distPaths.vendorJsFile;

      const vendorObject = getVendorFiles(files, isMainVendorFile);

      return concat(tree, {
        inputFiles: vendorObject.inputFiles,
        headerFiles: vendorObject.headerFiles,
        footerFiles: vendorObject.footerFiles,
        outputFile: importPath,
        annotation: `Package ${importPath}`,
        separator: '\n;',
        sourceMapConfig: this.sourcemaps,
      });
    });

    return mergeTrees(vendorTrees, {
      overwrite: true,
      annotation: 'Packaged Vendor Javascript',
    });
  }
};