UNPKG

ember-cli-ajh

Version:

Command line tool for developing ambitious ember.js apps

1,565 lines (1,318 loc) 40.5 kB
/* global require, module, escape */ 'use strict'; /** @module ember-cli */ var fs = require('fs'); var existsSync = require('exists-sync'); var path = require('path'); var p = require('ember-cli-preprocess-registry/preprocessors'); var chalk = require('chalk'); var escapeRegExp = require('escape-string-regexp'); var EOL = require('os').EOL; var Project = require('../models/project'); var cleanBaseURL = require('clean-base-url'); var SilentError = require('silent-error'); var preprocessJs = p.preprocessJs; var preprocessCss = p.preprocessCss; var isType = p.isType; var preprocessTemplates = p.preprocessTemplates; var preprocessMinifyCss = p.preprocessMinifyCss; var Babel = require('broccoli-babel-transpiler'); var concatFilesWithSourcemaps = require('broccoli-sourcemap-concat'); var ConfigReplace = require('broccoli-config-replace'); var ConfigLoader = require('broccoli-config-loader'); var mergeTrees = require('./merge-trees'); var WatchedDir = require('broccoli-source').WatchedDir; var UnwatchedDir = require('broccoli-source').UnwatchedDir; var defaults = require('merge-defaults'); var merge = require('lodash/object/merge'); var omit = require('lodash/object/omit'); var ES3SafeFilter = require('broccoli-es3-safe-recast'); var Funnel = require('broccoli-funnel'); module.exports = EmberApp; /** EmberApp is the main class Ember CLI uses to manage the Broccoli trees for your application. It is very tightly integrated with Broccoli and has an `toTree()` method you can use to get the entire tree for your application. Available init options: - es3Safe, defaults to `true`, - storeConfigInMeta, defaults to `true`, - autoRun, defaults to `true`, - outputPaths, defaults to `{}`, - minifyCSS, defaults to `{enabled: !!isProduction,options: { relativeTo: 'app/styles' }}, - minifyJS, defaults to `{enabled: !!isProduction}, - loader, defaults to this.bowerDirectory + '/loader.js/loader.js', - sourcemaps, defaults to `{}`, - trees, defaults to `{},` - jshintrc, defaults to `{},` - vendorFiles, defaults to `{}` @class EmberApp @constructor @param {Object} options Configuration options */ function EmberApp() { var args = []; var options = {}; for (var i = 0, l = arguments.length; i < l; i++) { args.push(arguments[i]); } if (args.length === 1) { options = args[0]; } else if (args.length > 1) { args.reverse(); options = defaults.apply(null, args); } this._initProject(options); this.env = EmberApp.env(); this.name = options.name || this.project.name(); this.registry = options.registry || p.defaultRegistry(this); this.bowerDirectory = this.project.bowerDirectory; var isProduction = this.env === 'production'; this._initTestsAndHinting(options, isProduction); this._initOptions(options, isProduction); this._initVendorFiles(); this.legacyFilesToAppend = []; this.vendorStaticStyles = []; this.otherAssetPaths = []; this.legacyTestFilesToAppend = []; this.vendorTestStaticStyles = []; this.trees = this.options.trees; this.populateLegacyFiles(); p.setupRegistry(this); this._notifyAddonIncluded(); } /** @private @method _initTestsAndHinting @param {Object} options @param {Boolean} isProduction */ EmberApp.prototype._initTestsAndHinting = function(options, isProduction) { var testsEnabledDefault = process.env.EMBER_CLI_TEST_COMMAND || !isProduction; this.tests = options.hasOwnProperty('tests') ? options.tests : testsEnabledDefault; this.hinting = options.hasOwnProperty('hinting') ? options.hinting : testsEnabledDefault; }; /** @private @method _initProject @param {Object} options */ EmberApp.prototype._initProject = function(options) { this.project = options.project || Project.closestSync(process.cwd()); if (options.configPath) { this.project.configPath = function() { return options.configPath; }; } }; /** @private @method _initOptions @param {Object} options @param {Boolean} isProduction */ EmberApp.prototype._initOptions = function(options, isProduction) { var babelOptions = { babel: {} }; if (this._addonInstalled('ember-cli-babel')) { var amdNameResolver = require('amd-name-resolver'); babelOptions = { babel: { compileModules: true, modules: 'amdStrict', moduleIds: true, resolveModuleSource: amdNameResolver } }; } var config = { es3Safe: true, storeConfigInMeta: true, autoRun: true, outputPaths: {}, minifyCSS: { enabled: !!isProduction, options: { relativeTo: 'app/styles' } }, minifyJS: { enabled: !!isProduction }, loader: this.bowerDirectory + '/loader.js/loader.js', sourcemaps: {}, trees: {}, jshintrc: {}, 'ember-cli-qunit': { disableContainerStyles: false } }; this.options = defaults(options, config, babelOptions); // needs a deeper merge than is provided above this.options.outputPaths = defaults(this.options.outputPaths, { app: { html: 'index.html', css: { 'app': '/assets/' + this.name + '.css' }, js: '/assets/' + this.name + '.js' }, tests: { js: '/assets/tests.js' }, vendor: { css: '/assets/vendor.css', js: '/assets/vendor.js' }, testSupport: { css: '/assets/test-support.css', js: { testSupport: '/assets/test-support.js', testLoader: '/assets/test-loader.js' } } }); this.options.sourcemaps = defaults(this.options.sourcemaps, { enabled: !isProduction, extensions: ['js'] }); // For now we must disable Babel sourcemaps due to unforseen // performance regressions. this.options.babel.sourceMaps = false; this.options.trees = defaults(this.options.trees, { app: new WatchedDir('app'), tests: new WatchedDir('tests'), // these are contained within app/ no need to watch again // (we should probably have the builder or the watcher dedup though) styles: new UnwatchedDir('app/styles'), templates: existsSync('app/templates') ? new UnwatchedDir('app/templates') : null, // do not watch vendor/ or bower's default directory by default bower: this.project._watchmanInfo.enabled ? this.bowerDirectory : new UnwatchedDir(this.bowerDirectory), vendor: existsSync('vendor') ? new UnwatchedDir('vendor') : null, public: existsSync('public') ? new WatchedDir('public') : null }); this.options.jshintrc = defaults(this.options.jshintrc, { app: this.project.root, tests: path.join(this.project.root, 'tests'), }); }; /** @private @method _initVendorFiles */ EmberApp.prototype._initVendorFiles = function() { // in Ember 1.10 and higher `ember.js` is deprecated in favor of // the more aptly named `ember.debug.js`. var defaultDevelopmentEmber = this.bowerDirectory + '/ember/ember.debug.js'; if (!existsSync(path.join(this.project.root, defaultDevelopmentEmber))) { defaultDevelopmentEmber = this.bowerDirectory + '/ember/ember.js'; } var handlebarsVendorFiles; if ('handlebars' in this.project.bowerDependencies()) { handlebarsVendorFiles = { development: this.bowerDirectory + '/handlebars/handlebars.js', production: this.bowerDirectory + '/handlebars/handlebars.runtime.js' }; } else { handlebarsVendorFiles = null; } this.vendorFiles = omit(merge({ 'loader.js': this.options.loader, 'jquery.js': this.bowerDirectory + '/jquery/dist/jquery.js', 'handlebars.js': handlebarsVendorFiles, 'ember.js': { development: defaultDevelopmentEmber, production: this.bowerDirectory + '/ember/ember.prod.js' }, 'ember-testing.js': [ this.bowerDirectory + '/ember/ember-testing.js', { type: 'test' } ], 'app-shims.js': [ this.bowerDirectory + '/ember-cli-shims/app-shims.js', { exports: { ember: ['default'] } } ], 'ember-resolver.js': [ this.bowerDirectory + '/ember-resolver/dist/modules/ember-resolver.js', { exports: { 'ember/resolver': ['default'] } } ], 'ember-load-initializers.js': [ this.bowerDirectory + '/ember-load-initializers/ember-load-initializers.js', { exports: { 'ember/load-initializers': ['default'] } } ] }, this.options.vendorFiles), function(value) { return value === null; }); 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']; } // this is needed to support versions of Ember older than // 1.8.0 (when ember-testing.js was added to the deployment) if (!existsSync(this.vendorFiles['ember-testing.js'][0])) { delete this.vendorFiles['ember-testing.js']; } }; /** Returns the environment name @public @static @method env @return {String} Environment name */ EmberApp.env = function(){ return process.env.EMBER_ENV || 'development'; }; /** Provides a broccoli files concatenation filter that's configured properly for this application. @method concatFiles @param tree @param options @return */ EmberApp.prototype.concatFiles = function(tree, options) { options.sourceMapConfig = this.options.sourcemaps; return concatFilesWithSourcemaps(tree, options); }; /** @private @method _notifyAddonIncluded */ EmberApp.prototype._notifyAddonIncluded = function() { this.initializeAddons(); this.project.addons = this.project.addons.filter(function(addon) { addon.app = this; if (!addon.isEnabled || addon.isEnabled()) { if (addon.included) { addon.included(this); } return addon; } }, this); }; /** Loads and initializes addons for this project. Calls initializeAddons on the Project. @private @method initializeAddons */ EmberApp.prototype.initializeAddons = function() { this.project.initializeAddons(); }; /** Returns a list of trees for a given type, returned by all addons. @private @method addonTreesFor @param {String} type Type of tree @return {Array} List of trees */ EmberApp.prototype.addonTreesFor = function(type) { return this.project.addons.map(function(addon) { if (addon.treeFor) { return addon.treeFor(type); } }).filter(Boolean); }; /** Runs addon postprocessing on a given tree and returns the processed tree. This enables addons to do process immediately **after** the preprocessor for a given type is run, but before concatenation occurs. If an addon wishes to apply a transform before the preprocessors run, they can instead implement the preprocessTree hook. To utilize this addons implement `postprocessTree` hook. An example, would be to apply some broccoli transform on all JS files, but only after the existing pre-processors have fun. ```js module.exports = { name: 'my-cool-addon', postprocessTree: function(type, tree) { if (type === 'js') { return someBroccoliTransform(tree); } return tree; } } ``` @private @method addonPostprocessTree @param {String} type Type of tree @param {Tree} tree Tree to process @return {Tree} Processed tree */ EmberApp.prototype.addonPostprocessTree = function(type, tree) { var workingTree = tree; this.project.addons.forEach(function(addon) { if (addon.postprocessTree) { workingTree = addon.postprocessTree(type, workingTree); } }); return workingTree; }; /** Runs addon postprocessing on a given tree and returns the processed tree. This enables addons to do process immediately **before** the preprocessor for a given type is run, but before concatenation occurs. If an addon wishes to apply a transform after the preprocessors run, they can instead implement the postprocessTree hook. To utilize this addons implement `postprocessTree` hook. An example, would be to remove some set of files before the preprocessors run. ```js var stew = require('broccoli-stew'); module.exports = { name: 'my-cool-addon', preprocessTree: function(type, tree) { if (type === 'js' && type === 'template') { return stew.rm(tree, someGlobPattern); } return tree; } } ``` @private @method addonPreprocessTree @param {String} type Type of tree @param {Tree} tree Tree to process @return {Tree} Processed tree */ EmberApp.prototype.addonPreprocessTree = function(type, tree) { var workingTree = tree; this.project.addons.forEach(function(addon) { if (addon.preprocessTree) { workingTree = addon.preprocessTree(type, workingTree); } }); return workingTree; }; /** Runs addon lintTree hooks and returns a single tree containing all their output. @private @method addonLintTree @param {String} type Type of tree @param {Tree} tree Tree to process @return {Tree} Processed tree */ EmberApp.prototype.addonLintTree = function(type, tree) { var output = this.project.addons.map(function(addon) { if (addon.lintTree) { return addon.lintTree(type, tree); } }).filter(Boolean); return mergeTrees(output, { overwrite: true, annotation: 'TreeMerger (lint)' }); }; /** Imports legacy imports in this.vendorFiles @private @method populateLegacyFiles */ EmberApp.prototype.populateLegacyFiles = function () { var name; for (name in this.vendorFiles) { var args = this.vendorFiles[name]; if (args === null) { continue; } this.import.apply(this, [].concat(args)); } }; /** Returns the tree for app/index.html @private @method index @return {Tree} Tree for app/index.html */ EmberApp.prototype.index = function() { var htmlName = this.options.outputPaths.app.html; var files = [ 'index.html' ]; var index = new Funnel(this.trees.app, { files: files, getDestinationPath: function(relativePath) { if (relativePath === 'index.html') { relativePath = htmlName; } return relativePath; }, annotation: 'Funnel: index.html' }); return new ConfigReplace(index, this._configTree(), { configPath: path.join(this.name, 'config', 'environments', this.env + '.json'), files: [ htmlName ], patterns: this._configReplacePatterns() }); }; /** @private @method _filterAppTree @return tree */ EmberApp.prototype._filterAppTree = function() { if (this._cachedFilterAppTree) { return this._cachedFilterAppTree; } var podPatterns = this._podTemplatePatterns(); var excludePatterns = podPatterns.concat([ // note: do not use path.sep here Funnel uses // walk-sync which always joins with `/` (not path.sep) 'styles/**/*', 'templates/**/*', ]); this._cachedFilterAppTree = new Funnel(this.trees.app, { exclude: excludePatterns, annotation: 'Funnel: Filtered App' }); return this._cachedFilterAppTree; }; /** @private @method _configReplacePatterns @return */ EmberApp.prototype._configReplacePatterns = function() { return [{ match: /\{\{EMBER_ENV\}\}/g, replacement: calculateEmberENV }, { match: /\{\{content-for ['"](.+)["']\}\}/g, replacement: this.contentFor.bind(this) }, { match: /\{\{MODULE_PREFIX\}\}/g, replacement: calculateModulePrefix }]; }; /** Returns the tree for /tests/index.html @private @method testIndex @return {Tree} Tree for /tests/index.html */ EmberApp.prototype.testIndex = function() { var index = new Funnel(this.trees.tests, { srcDir: '/', files: ['index.html'], destDir: '/tests', annotation: 'Funnel (test index)' }); return new ConfigReplace(index, this._configTree(), { configPath: path.join(this.name, 'config', 'environments', 'test.json'), files: [ 'tests/index.html' ], env: 'test', patterns: this._configReplacePatterns() }); }; /** Returns the tree for /public @private @method publicTree @return {Tree} Tree for /public */ EmberApp.prototype.publicTree = function() { var trees = this.addonTreesFor('public'); if (this.trees.public) { trees.push(this.trees.public); } return mergeTrees(trees, { overwrite: true, annotation: 'TreeMerge (public)' }); }; /** @private @method _processedAppTree @return */ EmberApp.prototype._processedAppTree = function() { var addonTrees = this.addonTreesFor('app'); var mergedApp = mergeTrees(addonTrees.concat(this._filterAppTree()), { overwrite: true, annotation: 'TreeMerger (app)' }); return new Funnel(mergedApp, { srcDir: '/', destDir: this.name, annotation: 'ProcessedAppTree' }); }; /** @private @method _processedTemplatesTree @return */ EmberApp.prototype._processedTemplatesTree = function() { var addonTrees = this.addonTreesFor('templates'); var mergedTrees = this.trees.templates ? addonTrees.concat(this.trees.templates) : addonTrees; var mergedTemplates = mergeTrees(mergedTrees, { overwrite: true, annotation: 'TreeMerger (templates)' }); var standardTemplates = new Funnel(mergedTemplates, { srcDir: '/', destDir: this.name + '/templates', annotation: 'ProcessedTemplateTree' }); var podTemplates = new Funnel(this.trees.app, { include: this._podTemplatePatterns(), exclude: [ 'templates/**/*' ], destDir: this.name + '/', annotation: 'Funnel: Pod Templates' }); var templates = this.addonPreprocessTree('template', mergeTrees([ standardTemplates, podTemplates ], { annotation: 'addonPreprocessTree(template)' })); return this.addonPostprocessTree('template', preprocessTemplates(templates, { registry: this.registry, annotation: 'TreeMerger (pod & standard templates)' })); }; /** @private @method _podTemplatePatterns @returns Array An array of regular expressions. */ EmberApp.prototype._podTemplatePatterns = function() { return this.registry.extensionsForType('template').map(function(extension) { return '**/*/template.' + extension; }); }; /** @private @method _processedTestsTree @return */ EmberApp.prototype._processedTestsTree = function() { var addonTrees = this.addonTreesFor('test-support'); var mergedTests = mergeTrees(addonTrees.concat(this.trees.tests), { overwrite: true, annotation: 'TreeMerger (tests)' }); return new Funnel(mergedTests, { srcDir: '/', destDir: this.name + '/tests', annotation: 'ProcessedTestTree' }); }; /** @private @method _processedBowerTree @return */ EmberApp.prototype._processedBowerTree = function() { if(this._cachedBowerTree) { return this._cachedBowerTree; } // do not attempt to merge bower and vendor together // if they are the same tree if (this.bowerDirectory === 'vendor') { return; } this._cachedBowerTree = new Funnel(this.trees.bower, { srcDir: '/', destDir: this.bowerDirectory + '/', annotation: 'Funnel (bower)' }); return this._cachedBowerTree; }; /** */ EmberApp.prototype._addonTree = function _addonTree() { if (this._cachedAddonTree) { return this._cachedAddonTree; } var addonTrees = mergeTrees(this.addonTreesFor('addon'), { overwrite: true, annotation: 'TreeMerger (addons)' }); var addonES6 = new Funnel(addonTrees, { srcDir: 'modules', allowEmpty: true, annotation: 'Funnel: Addon JS' }); var addonReexports = new Funnel(addonTrees, { srcDir: 'reexports', allowEmpty: true, annotation: 'Funnel: Addon Re-exports' }); var transpiledAddonTree = new Babel(addonES6, this._prunedBabelOptions()); var reexportsAndTranspiledAddonTree = mergeTrees([ transpiledAddonTree, addonReexports ], { annotation: 'TreeMerger: (re-exports)' }); return this._cachedAddonTree = [ this.concatFiles(addonTrees, { inputFiles: ['**/*.css'], outputFile: '/addons.css', allowNone: true, annotation: 'Concat: Addon CSS' }), this.concatFiles(reexportsAndTranspiledAddonTree, { inputFiles: ['**/*.js'], outputFile: '/addons.js', allowNone: true, annotation: 'Concat: Addon JS' }) ]; }; /** @private @method _processedVendorTree @return */ EmberApp.prototype._processedVendorTree = function() { if(this._cachedVendorTree) { return this._cachedVendorTree; } var trees = this._addonTree(); trees = trees.concat(this.addonTreesFor('vendor')); if (this.trees.vendor) { trees.push(this.trees.vendor); } var mergedVendor = mergeTrees(trees, { overwrite: true, annotation: 'TreeMerger (vendor)' }); this._cachedVendorTree = new Funnel(mergedVendor, { srcDir: '/', destDir: 'vendor/', annotation: 'Funnel (vendor)' }); return this._cachedVendorTree; }; /** @private @method _processedExternalTree @return */ EmberApp.prototype._processedExternalTree = function() { if (this._cachedExternalTree) { return this._cachedExternalTree; } var vendor = this._processedVendorTree(); var bower = this._processedBowerTree(); var trees = [vendor]; if (bower) { trees.unshift(bower); } return this._cachedExternalTree = mergeTrees(trees, { annotation: 'TreeMerger (ExternalTree)' }); }; /** @private @method _configTree @return */ EmberApp.prototype._configTree = function() { if (this._cachedConfigTree) { return this._cachedConfigTree; } var configPath = this.project.configPath(); var configTree = new ConfigLoader(path.dirname(configPath), { env: this.env, tests: this.tests, project: this.project }); this._cachedConfigTree = new Funnel(configTree, { srcDir: '/', destDir: this.name + '/config', annotation: 'Funnel (config)' }); return this._cachedConfigTree; }; /** @private @method _processedEmberCLITree @return */ EmberApp.prototype._processedEmberCLITree = function() { if (this._cachedEmberCLITree) { return this._cachedEmberCLITree; } var files = [ 'vendor-prefix.js', 'vendor-suffix.js', 'app-prefix.js', 'app-suffix.js', 'app-boot.js', 'test-support-prefix.js', 'test-support-suffix.js', 'tests-suffix.js' ]; var emberCLITree = new ConfigReplace(new UnwatchedDir(__dirname), this._configTree(), { configPath: path.join(this.name, 'config', 'environments', this.env + '.json'), files: files, patterns: this._configReplacePatterns() }); return this._cachedEmberCLITree = new Funnel(emberCLITree, { files: files, srcDir: '/', destDir: '/vendor/ember-cli/', annotation: 'Funnel (ember-cli-tree)' }); }; /** Returns the tree for the app and its dependencies @private @method appAndDependencies @return {Tree} Merged tree */ EmberApp.prototype.appAndDependencies = function() { var sourceTrees = []; var config = this._configTree(); var app; var templates = this._processedTemplatesTree(); if (!this._addonInstalled('ember-cli-babel')) { app = this.addonPreprocessTree('js', this._processedAppTree()); sourceTrees.push(templates); if (this.options.es3Safe) { app = new ES3SafeFilter(app); } } else { app = this.addonPreprocessTree('js', mergeTrees([ this._processedAppTree(), templates ].concat(sourceTrees), { annotation: 'TreeMerger (preprocessedApp & templates)', overwrite: true })); } var external = this._processedExternalTree(); var preprocessedApp = preprocessJs(app, '/', this.name, { registry: this.registry }); var postprocessedApp = this.addonPostprocessTree('js', preprocessedApp); sourceTrees = sourceTrees.concat([ external, postprocessedApp, config ]); var emberCLITree = this._processedEmberCLITree(); sourceTrees.push(emberCLITree); return mergeTrees(sourceTrees, { overwrite: true, annotation: 'TreeMerger (appAndDependencies)' }); }; EmberApp.prototype.test = function() { var tests = this.addonPreprocessTree('test', this._processedTestsTree()); var preprocessedTests = preprocessJs(tests, '/tests', this.name, { registry: this.registry }); var coreTestTree = this.addonPostprocessTree('test', preprocessedTests); var appTestTree = this.appTests(coreTestTree); var testFilesTree = this.testFiles(coreTestTree); return mergeTrees([appTestTree, testFilesTree]); }; /** @private @method appTests */ EmberApp.prototype.appTests = function(coreTestTree) { var appTestTrees = [coreTestTree]; if (this.hinting) { var jshintedApp = this.addonLintTree('app', this._filterAppTree()); var jshintedTests = this.addonLintTree('tests', this.trees.tests); jshintedApp = new Babel(new Funnel(jshintedApp, { srcDir: '/', destDir: this.name + '/tests/', annotation: 'Funnel (jshint app)' }), this._prunedBabelOptions()); jshintedTests = new Babel(new Funnel(jshintedTests, { srcDir: '/', destDir: this.name + '/tests/', annotation: 'Funnel (jshint tests)' }), this._prunedBabelOptions()); appTestTrees.push(jshintedApp); appTestTrees.push(jshintedTests); } appTestTrees.push(this._processedEmberCLITree()); appTestTrees = mergeTrees(appTestTrees, { overwrite: true, annotation: 'TreeMerger (appTestTrees)' }); return this.concatFiles(appTestTrees, { inputFiles: [ this.name + '/tests/**/*.js' ], footerFiles: [ 'vendor/ember-cli/tests-suffix.js' ], outputFile: this.options.outputPaths.tests.js, annotation: 'Concat: App Tests' }); }; /** * @private * @param {String} addonName The name of the addon we are checking to see if it's installed * @return {Boolean} */ EmberApp.prototype._addonInstalled = function(addonName) { return !!this.registry.availablePlugins[addonName]; }; /** * @private * * Prunes ember-cli-babel options * @return {Object} The pruned babel-options */ EmberApp.prototype._prunedBabelOptions = function() { var babelOptions = merge({}, this.options.babel); delete babelOptions.compileModules; return babelOptions; }; /** Returns the tree for javascript files @private @method javascript @return {Tree} Merged tree */ EmberApp.prototype.javascript = function() { var applicationJs = this.appAndDependencies(); var legacyFilesToAppend = this.legacyFilesToAppend; var appOutputPath = this.options.outputPaths.app.js; var appJs = applicationJs; // Note: If ember-cli-babel is installed we have already performed the transpilation at this point if (!this._addonInstalled('ember-cli-babel')) { appJs = new Babel( new Funnel(applicationJs, { include: [escapeRegExp(this.name + '/') + '**/*.js'], annotation: 'Funnel: App JS Files' }), merge(this._prunedBabelOptions()) ); } appJs = mergeTrees([ appJs, this._processedEmberCLITree() ], { annotation: 'TreeMerger (appJS & processedEmberCLITree)', overwrite: true }); appJs = this.concatFiles(appJs, { inputFiles: [this.name + '/**/*.js'], headerFiles: [ 'vendor/ember-cli/app-prefix.js' ], footerFiles: [ 'vendor/ember-cli/app-suffix.js', 'vendor/ember-cli/app-boot.js' ], outputFile: appOutputPath, annotation: 'Concat: App' }); var inputFiles = ['vendor/ember-cli/vendor-prefix.js'] .concat(legacyFilesToAppend) .concat('vendor/addons.js') .concat('vendor/ember-cli/vendor-suffix.js'); var vendor = this.concatFiles(applicationJs, { inputFiles: inputFiles, outputFile: this.options.outputPaths.vendor.js, separator: EOL + ';', annotation: 'Concat: Vendor' }); return mergeTrees([ vendor, appJs ], { annotation: 'TreeMerger (vendor & appJS)' }); }; /** Returns the tree for styles @private @method styles @return {Tree} Merged tree for styles */ EmberApp.prototype.styles = function() { if (this._processedStylesTree) { return this._processedStylesTree; } if (existsSync('app/styles/' + this.name + '.css')) { throw new SilentError('Style file cannot have the name of the application - ' + this.name); } var addonTrees = this.addonTreesFor('styles'); var external = this._processedExternalTree(); var styles = new Funnel(this.trees.styles, { srcDir: '/', destDir: '/app/styles', annotation: 'Funnel (styles)' }); var trees = [external].concat(addonTrees); trees.push(styles); var stylesAndVendor = this.addonPreprocessTree('css', mergeTrees(trees, { annotation: 'TreeMerger (stylesAndVendor)', overwrite: true })); var options = { outputPaths: this.options.outputPaths.app.css }; options.registry = this.registry; var preprocessedStyles = preprocessCss(stylesAndVendor, '/app/styles', '/assets', options); var vendorStyles = this.addonPreprocessTree('css', this.concatFiles(stylesAndVendor, { inputFiles: this.vendorStaticStyles.concat(['vendor/addons.css']), outputFile: this.options.outputPaths.vendor.css, annotation: 'Concat: Vendor Styles' })); if (this.options.minifyCSS.enabled === true) { options = this.options.minifyCSS.options || {}; options.registry = this.registry; preprocessedStyles = preprocessMinifyCss(preprocessedStyles, options); vendorStyles = preprocessMinifyCss(vendorStyles, options); } var mergedTrees = mergeTrees([ preprocessedStyles, vendorStyles ], { annotation: 'styles' }); return this._processedStylesTree = this.addonPostprocessTree('css', mergedTrees); }; /** Returns the tree for test files @private @method testFiles @return {Tree} Merged tree for test files */ EmberApp.prototype.testFiles = function(coreTestTree) { var testSupportPath = this.options.outputPaths.testSupport.js; var testLoaderPath = this.options.outputPaths.testSupport.js.testLoader; testSupportPath = testSupportPath.testSupport || testSupportPath; var external = this._processedExternalTree(); var emberCLITree = this._processedEmberCLITree(); var testJs = this.concatFiles(mergeTrees([external, coreTestTree]), { inputFiles: this.legacyTestFilesToAppend, outputFile: testSupportPath, annotation: 'Concat: Test Support JS' }); testJs = this.concatFiles(mergeTrees([testJs, emberCLITree]), { inputFiles: [ 'vendor/ember-cli/test-support-prefix.js', testSupportPath.slice(1), 'vendor/ember-cli/test-support-suffix.js' ], outputFile: testSupportPath, annotation: 'Concat: Test Support Suffix' }); var testemPath = path.join(__dirname, 'testem'); testemPath = path.dirname(testemPath); var testemTree = new Funnel(new UnwatchedDir(testemPath), { files: ['testem.js'], srcDir: '/', destDir: '/', annotation: 'Funnel (testem)' }); if (this.options.fingerprint && this.options.fingerprint.exclude) { this.options.fingerprint.exclude.push('testem'); } var testLoader = new Funnel(external, { files: ['test-loader.js'], srcDir: '/' + this.bowerDirectory + '/ember-cli-test-loader', destDir: path.dirname(testLoaderPath), annotation: 'Funnel (testLoader)' }); var sourceTrees = [ testJs, testLoader, testemTree ]; if (this.vendorTestStaticStyles.length > 0) { sourceTrees.push( this.concatFiles(external, { inputFiles: this.vendorTestStaticStyles, outputFile: this.options.outputPaths.testSupport.css, annotation: 'Concat: Test Support CSS' }) ); } return mergeTrees(sourceTrees, { overwrite: true, annotation: 'TreeMerger (testFiles)' }); }; /** Returns the tree for the additional assets which are not in one of the default trees. @private @method otherAssets @return {Tree} Merged tree for other assets */ EmberApp.prototype.otherAssets = function() { var external = this._processedExternalTree(); var otherAssetTrees = this.otherAssetPaths.map(function (path) { return new Funnel(external, { srcDir: path.src, files: [path.file], destDir: path.dest, annotation: 'Funnel (otherAssets)' }); }); return mergeTrees(otherAssetTrees, { annotation: 'TreeMerger (otherAssetTrees)' }); }; /** @public @method dependencies @return {Object} Alias to the project's dependencies function */ EmberApp.prototype.dependencies = function(pkg) { return this.project.dependencies(pkg); }; /** Imports an asset into the application. Options: - type - Either 'vendor' or 'test', defaults to 'vendor' - prepend - Whether or not this asset should be prepended, defaults to false - destDir - Destination directory, defaults to the name of the directory the asset is in @public @method import @param {(Object|String)} asset Either a path to the asset or an object with envirnoment names and paths as key-value pairs. @param {Object=} options Options object */ EmberApp.prototype.import = function(asset, options) { var assetPath = this._getAssetPath(asset); if (!assetPath) { return; } options = defaults(options || {}, { type: 'vendor', prepend: false }); var directory = path.dirname(assetPath); var subdirectory = directory.replace(new RegExp('^vendor/|' + this.bowerDirectory), ''); var extension = path.extname(assetPath); if (!extension) { throw new Error('You must pass a file to `app.import`. For directories specify them to the constructor under the `trees` option.'); } this._import( assetPath, options, directory, subdirectory, extension ); }; /** @private @method _import @param {String} assetPath @param {Object} options @param {String} directory @param {String} subdirectory @param {String} extension */ EmberApp.prototype._import = function(assetPath, options, directory, subdirectory, extension) { var basename = path.basename(assetPath); if (isType(assetPath, 'js', {registry: this.registry})) { if(options.type === 'vendor') { if (options.prepend) { this.legacyFilesToAppend.unshift(assetPath); } else { this.legacyFilesToAppend.push(assetPath); } } else if (options.type === 'test' ) { this.legacyTestFilesToAppend.push(assetPath); } else { throw new Error( 'You must pass either `vendor` or `test` for options.type in your call to `app.import` for file: '+basename ); } } else if (extension === '.css') { if(options.type === 'vendor') { this.vendorStaticStyles.push(assetPath); } else { this.vendorTestStaticStyles.push(assetPath); } } else { var destDir = options.destDir; if (destDir === '') { destDir = '/'; } this.otherAssetPaths.push({ src: directory, file: basename, dest: destDir || subdirectory }); } }; /** @private @method _getAssetPath @param {(Object|String)} asset @return {(String|undefined)} assetPath */ EmberApp.prototype._getAssetPath = function(asset) { /** @type {String} */ var assetPath; if (typeof asset === 'object') { if (this.env in asset) { assetPath = asset[this.env]; } else { assetPath = asset.development; } } else { assetPath = asset; } if (!assetPath) { return; } assetPath = assetPath.replace(path.sep, '/'); if (assetPath.split('/').length < 2) { console.log(chalk.red('Using `app.import` with a file in the root of `vendor/` causes a significant performance penalty. Please move `'+ assetPath + '` into a subdirectory.')); } if (/[\*\,]/.test(assetPath)) { throw new Error('You must pass a file path (without glob pattern) to `app.import`. path was: `' + assetPath + '`'); } return assetPath; }; /** Returns an array of trees for this application @private @method toArray @return {Array} An array of trees */ EmberApp.prototype.toArray = function() { var sourceTrees = [ this.index(), this.javascript(), this.styles(), this.otherAssets(), this.publicTree() ]; if (this.tests) { sourceTrees = sourceTrees.concat(this.testIndex(), this.test()); } return sourceTrees; }; /** Returns the merged tree for this application @public @method toTree @param {Array} additionalTrees Array of additional trees to merge @return {Tree} Merged tree for this application */ EmberApp.prototype.toTree = function(additionalTrees) { var tree = mergeTrees(this.toArray().concat(additionalTrees || []), { overwrite: true, annotation: 'TreeMerger (allTrees)' }); return this.addonPostprocessTree('all', tree); }; /** Returns the content for a specific type (section) for index.html. Currently supported types: - 'head' - 'config-module' - 'app' - 'head-footer' - 'test-header-footer' - 'body-footer' - 'test-body-footer' Addons can also implement this method and could also define additional types (eg. 'some-addon-section'). @private @method contentFor @param {Object} config Application configuration @param {RegExp} match Regular expression to match against @param {String} type Type of content @return {String} The content. */ EmberApp.prototype.contentFor = function(config, match, type) { var content = []; switch (type) { case 'head': this._contentForHead(content, config); break; case 'config-module': this._contentForConfigModule(content, config); break; case 'app-boot': this._contentForAppBoot(content, config); break; case 'test-body-footer': this._contentForTestBodyFooter(content); break; } content = this.project.addons.reduce(function(content, addon) { var addonContent = addon.contentFor ? addon.contentFor(type, config, content) : null; if (addonContent) { return content.concat(addonContent); } return content; }, content); return content.join('\n'); }; /** @private @method _contentForTestBodyFooter @param {Array} content */ EmberApp.prototype._contentForTestBodyFooter = function(content) { content.push('<script>Ember.assert(\'The tests file was not loaded. Make sure your tests index.html includes "assets/tests.js".\', EmberENV.TESTS_FILE_LOADED);</script>'); }; /** @private @method _contentForHead @param {Array} content @param {Object} config */ EmberApp.prototype._contentForHead = function(content, config) { content.push(calculateBaseTag(config)); if (this.options.storeConfigInMeta) { content.push('<meta name="' + config.modulePrefix + '/config/environment" ' + 'content="' + escape(JSON.stringify(config)) + '" />'); } }; /** @private @method _contentForConfigModule @param {Array} content @param {Object} config */ EmberApp.prototype._contentForConfigModule = function(content, config) { if (this.options.storeConfigInMeta) { content.push('var prefix = \'' + config.modulePrefix + '\';'); content.push(fs.readFileSync(path.join(__dirname, 'app-config-from-meta.js'))); } else { content.push('return { \'default\': ' + JSON.stringify(config) + '};'); } }; /** @private @method _contentForAppBoot @param {Array} content @param {Object} config */ EmberApp.prototype._contentForAppBoot = function(content, config) { if (this.options.autoRun) { content.push('if (!runningTests) {'); content.push(' require("' + config.modulePrefix + '/app")["default"].create(' + calculateAppConfig(config) + ');'); content.push('}'); } }; /** Returns the <base> tag for index.html @param {Object} config Application configuration @return {String} Base tag or empty string */ function calculateBaseTag(config){ var baseURL = cleanBaseURL(config.baseURL); var locationType = config.locationType; if (locationType === 'hash') { return ''; } if (baseURL) { return '<base href="' + baseURL + '" />'; } else { return ''; } } function calculateEmberENV(config) { return JSON.stringify(config.EmberENV || {}); } function calculateAppConfig(config) { return JSON.stringify(config.APP || {}); } function calculateModulePrefix(config) { return config.modulePrefix; }