bricks-cli
Version:
Command line tool for developing ambitious ember.js apps
634 lines (521 loc) • 16.4 kB
JavaScript
/* global require, module */
'use strict';
var fs = require('fs');
var p = require('../preprocessors');
var chalk = require('chalk');
var Project = require('../models/project');
var cleanBaseURL = require('../utilities/clean-base-url');
var preprocessJs = p.preprocessJs;
var preprocessCss = p.preprocessCss;
var isType = p.isType;
var preprocessTemplates = p.preprocessTemplates;
var preprocessMinifyCss = p.preprocessMinifyCss;
var replace = require('broccoli-string-replace');
var compileES6 = require('broccoli-es6-concatenator');
var pickFiles = require('broccoli-static-compiler');
var jshintTrees = require('broccoli-jshint');
var concatFiles = require('./bricks-concat-source-map.js');
var remove = require('broccoli-file-remover');
var upstreamMergeTrees = require('broccoli-merge-trees');
var unwatchedTree = require('broccoli-unwatched-tree');
var uglifyJavaScript = require('broccoli-uglify-js');
var memoize = require('lodash-node/modern/functions').memoize;
var assign = require('lodash-node/modern/objects/assign');
var defaults = require('lodash-node/modern/objects/defaults');
var merge = require('lodash-node/modern/objects/merge');
var path = require('path');
var ES3SafeFilter = require('broccoli-es3-safe-recast');
function mergeTrees(inputTree, options) {
var tree = upstreamMergeTrees(inputTree, options);
tree.description = options && options.description;
return tree;
}
module.exports = EmberApp;
function EmberApp(options) {
options = options || {};
this.project = options.project || Project.closestSync(process.cwd());
this.env = process.env.EMBER_ENV || 'development';
this.name = options.name || this.project.name();
this.registry = options.registry || p.setupRegistry(this);
var isProduction = this.env === 'production';
this.tests = options.hasOwnProperty('tests') ? options.tests : !isProduction;
this.hinting = options.hasOwnProperty('hinting') ? options.hinting : !isProduction;
if (process.env.EMBER_CLI_TEST_COMMAND) {
this.tests = true;
}
this.options = merge(options, {
es3Safe: true,
wrapInEval: !isProduction,
minifyCSS: {
enabled: true,
options: { relativeTo: 'app/styles' }
},
minifyJS: {
enabled: true,
options: {
mangle: true,
compress: true
}
},
loader: 'vendor/loader/loader.js',
trees: {},
jshintrc: {},
getEnvJSON: this.project.require(options.environment || './config/environment'),
vendorFiles: {}
}, defaults);
this.vendorFiles = merge(options.vendorFiles, {
'loader.js': 'vendor/loader/loader.js',
'jquery.js': 'vendor/jquery/dist/jquery.js',
'handlebars.js': {
development: 'vendor/handlebars/handlebars.js',
production: 'vendor/handlebars/handlebars.runtime.js'
},
'ember.js': {
development: 'vendor/ember/ember.js',
production: 'vendor/ember/ember.prod.js'
},
'app-shims.js': [
'vendor/ember-cli-shims/app-shims.js', {
exports: {
ember: ['default']
}
}
],
'ember-resolver.js': [
'vendor/ember-resolver/dist/modules/ember-resolver.js', {
exports: {
'ember/resolver': ['default']
}
}
],
'ember-load-initializers.js': [
'vendor/ember-load-initializers/ember-load-initializers.js', {
exports: {
'ember/load-initializers': ['default']
}
}
],
'qunit.js' : [
'vendor/qunit/qunit/qunit.js', {
type: 'test '
}
],
'qunit.css': [
'vendor/qunit/qunit/qunit.css', {
type: 'test'
}
],
'qunit-notifications.js': [
'vendor/qunit-notifications/index.js', {
type: 'test'
}
]
}, defaults);
this.options.trees = merge(this.options.trees, {
app: 'app',
tests: 'tests',
// these are contained within app/ no need to watch again
styles: unwatchedTree('app/styles'),
templates: unwatchedTree('app/templates'),
// do not watch vendor/ by default
vendor: unwatchedTree('vendor'),
}, defaults);
this.options.jshintrc = merge(this.options.jshintrc, {
app: this.project.root,
tests: path.join(this.project.root, 'tests'),
}, defaults);
this.importWhitelist = {};
this.legacyFilesToAppend = [];
this.vendorStaticStyles = [];
this.otherAssetPaths = [];
this._importTrees = [];
this.legacyTestFilesToAppend = [];
this.vendorTestStaticStyles = [];
this.trees = this.options.trees;
this.populateLegacyFiles();
this._notifyAddonIncluded();
}
EmberApp.prototype._notifyAddonIncluded = function() {
this.project.initializeAddons();
this.project.addons.forEach(function(addon) {
if (addon.included) {
addon.included(this);
}
}, this);
};
EmberApp.prototype.addonTreesFor = function(type) {
return this.project.addons.map(function(addon) {
if (addon.treeFor) {
return addon.treeFor(type);
}
}, this).filter(Boolean);
};
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;
};
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));
}
};
EmberApp.prototype.index = memoize(function() {
var files = [
'index.html'
];
var index = pickFiles(this.trees.app, {
srcDir: '/',
files: files,
destDir: '/'
});
return injectENVJson(this.options.getEnvJSON, this.env, index, files);
});
EmberApp.prototype.testIndex = memoize(function() {
var index = pickFiles(this.trees.tests, {
srcDir: '/',
files: ['index.html'],
destDir: '/tests'
});
return injectENVJson(this.options.getEnvJSON, this.env, index, [
'tests/index.html'
]);
});
EmberApp.prototype.publicFolder = function() {
return 'public';
};
EmberApp.prototype._processedAppTree = memoize(function() {
var addonTrees = this.addonTreesFor('app');
var mergedApp = mergeTrees(addonTrees.concat(this.trees.app), {
overwrite: true,
description: 'TreeMerger (app)'
});
var mergedAppWithoutStylesAndTemplates = remove(mergedApp, {
paths: ['styles', 'templates']
});
return pickFiles(mergedAppWithoutStylesAndTemplates, {
srcDir: '/',
destDir: this.name
});
});
EmberApp.prototype._processedTemplatesTree = function() {
var addonTrees = this.addonTreesFor('templates');
var mergedTemplates = mergeTrees(addonTrees.concat(this.trees.templates), {
overwrite: true,
description: 'TreeMerger (templates)'
});
var templates = pickFiles(mergedTemplates, {
srcDir: '/',
destDir: this.name + '/templates'
});
return preprocessTemplates(templates);
};
EmberApp.prototype._processedTestsTree = memoize(function() {
return pickFiles(this.trees.tests, {
srcDir: '/',
destDir: this.name + '/tests'
});
});
EmberApp.prototype._processedVendorTree = memoize(function() {
var addonTrees = this.addonTreesFor('vendor');
var mergedVendor = mergeTrees(this._importTrees.concat(addonTrees, this.trees.vendor), {
overwrite: true,
description: 'TreeMerger (vendor)'
});
return pickFiles(mergedVendor, {
srcDir: '/',
destDir: 'vendor/'
});
});
EmberApp.prototype.appAndDependencies = memoize(function() {
var app = this._processedAppTree();
var templates = this._processedTemplatesTree();
if (this.options.es3Safe) {
app = new ES3SafeFilter(app);
}
var vendor = this._processedVendorTree();
var preprocessedApp = preprocessJs(app, '/', this.name);
var sourceTrees = [ vendor, preprocessedApp, templates ];
if (this.tests) {
var tests = this._processedTestsTree();
var preprocessedTests = preprocessJs(tests, '/tests', this.name);
sourceTrees.push(preprocessedTests);
if (this.hinting) {
var jshintedApp = jshintTrees(preprocessedApp, {
jshintrcPath: this.options.jshintrc.app,
description: 'JSHint - App'
});
var jshintedTests = jshintTrees(tests, {
jshintrcPath: this.options.jshintrc.tests,
description: 'JSHint - Tests'
});
jshintedApp = pickFiles(jshintedApp, {
srcDir: '/',
destDir: this.name + '/tests/'
});
jshintedTests = pickFiles(jshintedTests, {
srcDir: '/',
destDir: this.name + '/tests/'
});
sourceTrees.push(jshintedApp);
sourceTrees.push(jshintedTests);
}
}
var emberCLITree = pickFiles(unwatchedTree(__dirname), {
files: ['loader.js'],
srcDir: '/',
destDir: '/assets/ember-cli/'
});
sourceTrees.push(emberCLITree);
return mergeTrees(sourceTrees, {
overwrite: true,
description: 'TreeMerger (appAndDependencies)'
});
});
EmberApp.prototype.javascript = memoize(function() {
var applicationJs = this.appAndDependencies();
var legacyFilesToAppend = this.legacyFilesToAppend;
if (this.tests) {
this.import('vendor/ember-qunit/dist/named-amd/main.js', {
exports: {
'ember-qunit': [
'globalize',
'moduleFor',
'moduleForComponent',
'moduleForModel',
'test',
'setResolver'
]
}
});
this.import('vendor/ember-cli-shims/test-shims.js', {
exports: {
'qunit': ['default']
}
});
}
var es6 = compileES6(applicationJs, {
loaderFile: 'assets/ember-cli/loader.js',
ignoredModules: Object.keys(this.importWhitelist),
inputFiles: [
this.name + '/**/*.js'
],
wrapInEval: this.options.wrapInEval,
outputFile: '/assets/' + this.name + '.js',
});
var vendor = concatFiles(applicationJs, {
inputFiles: legacyFilesToAppend,
outputFile: '/assets/vendor.js',
separator: '\n;'
});
var vendorAndApp = mergeTrees([vendor, es6]);
if (this.env === 'production' && this.options.minifyJS.enabled === true) {
var options = this.options.minifyJS.options || {};
return uglifyJavaScript(vendorAndApp, options);
} else {
return vendorAndApp;
}
});
EmberApp.prototype.styles = memoize(function() {
var addonTrees = this.addonTreesFor('styles');
var vendor = this._processedVendorTree();
var styles = pickFiles(this.trees.styles, {
srcDir: '/',
destDir: '/app/styles'
});
var trees = [vendor].concat(addonTrees);
trees.push(styles);
var stylesAndVendor = mergeTrees(trees, {
description: 'TreeMerger (stylesAndVendor)'
});
var processedStyles = preprocessCss(stylesAndVendor, '/app/styles', '/assets');
var vendorStyles = concatFiles(stylesAndVendor, {
inputFiles: this.vendorStaticStyles,
outputFile: '/assets/vendor.css',
description: 'concatFiles - vendorStyles'
});
if (this.env === 'production' && this.options.minifyCSS.enabled === true) {
var options = this.options.minifyCSS.options || {};
processedStyles = preprocessMinifyCss(processedStyles, options);
vendorStyles = preprocessMinifyCss(vendorStyles, options);
}
return mergeTrees([
processedStyles,
vendorStyles
], {
description: 'styles'
});
});
EmberApp.prototype.testFiles = memoize(function() {
var vendor = this._processedVendorTree();
var testJs = concatFiles(vendor, {
inputFiles: this.legacyTestFilesToAppend,
outputFile: '/assets/test-support.js'
});
var testCss = concatFiles(vendor, {
inputFiles: this.vendorTestStaticStyles,
outputFile: '/assets/test-support.css'
});
var testemPath = path.join(__dirname, 'testem');
testemPath = path.dirname(testemPath);
var testemTree = pickFiles(unwatchedTree(testemPath), {
files: ['testem.js'],
srcDir: '/',
destDir: '/'
});
if (this.options.fingerprint && this.options.fingerprint.exclude) {
this.options.fingerprint.exclude.push('testem');
}
var iconsTree = pickFiles(this.trees.vendor, {
files: ['passed.png', 'failed.png'],
srcDir: '/ember-qunit-notifications',
destDir: '/assets/'
});
var testLoader = pickFiles(this.trees.vendor, {
files: ['test-loader.js'],
srcDir: '/ember-cli-test-loader',
destDir: '/assets/'
});
var sourceTrees = [
testJs,
testCss,
testemTree,
iconsTree,
testLoader
];
return mergeTrees(sourceTrees, {
overwrite: true,
description: 'TreeMerger (testFiles)'
});
});
EmberApp.prototype.otherAssets = memoize(function() {
var vendor = this._processedVendorTree();
var otherAssetTrees = this.otherAssetPaths.map(function (path) {
return pickFiles(vendor, {
srcDir: path.src,
files: [path.file],
destDir: path.dest
});
});
return mergeTrees(otherAssetTrees, {
description: 'TreeMerger (otherAssetTrees)'
});
});
EmberApp.prototype.import = function(asset, options) {
var assetPath;
if (typeof asset === 'object') {
assetPath = asset[this.env] || asset.development;
} else {
assetPath = asset;
}
if (!assetPath) {
return;
}
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.'));
}
options = defaults(options || {}, {
type: 'vendor'
});
if (/[\*\,]/.test(assetPath)) {
throw new Error('You must pass a file path (without glob pattern) to `app.import`. path was: `' + assetPath + '`');
}
var directory = path.dirname(assetPath);
var subdirectory = directory.replace(/^vendor\//, '');
var extension = path.extname(assetPath);
var basename = path.basename(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.');
}
if (fs.existsSync(directory) && this._importTrees.indexOf(directory) === -1) {
var assetTree = pickFiles(directory, {
srcDir: '/',
destDir: subdirectory
});
this._importTrees.push(assetTree);
}
if (isType(assetPath, 'js')) {
if(options.type === 'vendor') {
this.legacyFilesToAppend.push(assetPath);
} else {
this.legacyTestFilesToAppend.push(assetPath);
}
this.importWhitelist = assign(this.importWhitelist, options.exports || {});
} 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
});
}
};
EmberApp.prototype.toArray = function() {
var sourceTrees = [
this.index(),
this.javascript(),
this.styles(),
this.otherAssets()
];
var publicFolder = this.publicFolder();
if (fs.existsSync(publicFolder)) {
sourceTrees.push(publicFolder);
}
if (this.tests) {
sourceTrees = sourceTrees.concat(this.testIndex(), this.testFiles());
}
return sourceTrees;
};
EmberApp.prototype.toTree = function(additionalTrees) {
var tree = mergeTrees(this.toArray().concat(additionalTrees || []), {
overwrite: true,
description: 'TreeMerger (allTrees)'
});
return this.addonPostprocessTree('all', tree);
};
function injectENVJson(fn, env, tree, files) {
// TODO: real templating
var envJsonString = function(){
return JSON.stringify(fn(env));
};
var baseTag = function(){
var envJSON = fn(env);
var baseURL = cleanBaseURL(envJSON.baseURL);
var locationType = envJSON.locationType;
if (locationType === 'hash' || locationType === 'none') {
return '';
}
if (baseURL) {
return '<base href="' + baseURL + '" />';
} else {
return '';
}
};
return replace(tree, {
files: files,
patterns: [{
match: /\{\{ENV\}\}/g,
replacement: envJsonString
},
{
match: /\{\{BASE_TAG\}\}/g,
replacement: baseTag
}]
});
}