UNPKG

bricks-cli

Version:

Command line tool for developing ambitious ember.js apps

569 lines (468 loc) 13.7 kB
'use strict'; var FileInfo = require('./file-info'); var Promise = require('../ext/promise'); var any = require('lodash-node/compat/collections/some'); var chalk = require('chalk'); var fs = require('fs-extra'); var glob = require('glob'); var merge = require('lodash-node/compat/objects/merge'); var minimatch = require('minimatch'); var path = require('path'); var sequence = require('../utilities/sequence'); var stat = Promise.denodeify(fs.stat); var stringUtils = require('../utilities/string'); var uniq = require('lodash-node/compat/arrays/uniq'); var walkSync = require('walk-sync'); var writeFile = Promise.denodeify(fs.outputFile); module.exports = Blueprint; /* @class Blueprint @extends CoreObject @param {String} [blueprintPath] A `Blueprint` is a bundle of template files with optional install logic. Ember CLI uses blueprints to generate new projects via `ember new` and `ember init`, and project resources via `ember generate`. Blueprints follow a simple structure. Let's take the built-in `controller` blueprint as an example: ``` blueprints/controller ├── files │   ├── app │   │   └── controllers │   │   └── __name__.js │   └── tests │   └── unit │   └── controllers │   └── __name__-test.js └── index.js ``` ## Files `files` contains templates for the all the files to be installed into the target directory. The `__name__` placeholder is subtituted with the dasherized entity name at install time. For example, when the user invokes `ember generate controller foo` then `__name__` becomes `foo`. ## Template Variables (AKA Locals) Variables can be inserted into templates with `<%= someVariableName %>`. For example, in the built-in `util` blueprint `files/app/utils/__name__.js` looks like this: ```js export default function <%= camelizedModuleName %>() { return true; } ``` `<%= camelizedModuleName %>` is replaced with the real value at install time. The following template variables provided by default: - `dasherizedPackageName` - `classifiedPackageName` - `dasherizedModuleName` - `classifiedModuleName` - `camelizedModuleName` `packageName` is the project name as found in the project's `package.json`. `moduleName` is the name of the entity being generated. The mechanism for providing custom template variables is described below. ## Index.js `index.js` contains a subclass of `Blueprint`. Use this to customize installation behaviour. ```js var Blueprint = require('ember-cli/lib/models/blueprint'); module.exports = Blueprint.extend({ locals: function(options) { // Return custom template variables here. return {}; }, afterInstall: function(options) { // Perform extra work here. } }); ``` As shown above, there are two hooks available: `locals` and `afterInstall`. ## Locals Use `locals` to add custom tempate variables. The method recieves one argument: `options`. Options is an object containing general and entity-specific install options. When the following is called on the command line: ```sh $ ember generate controller foo type:array --dry-run ``` The object passed to `locals` looks like this: ```js { entity: { name: 'foo', options: { type: 'array' } }, dryRun: true } ``` This hook must return an object. It will be merged with the aforementioned default locals. ## afterInstall The `afterInstall` hook receives the same options as `locals`. Use it to perform any custom work after the files are installed. For example, the built-in `route` blueprint uses the `afterInstall` hook to add relevant route declarations to `app/router.js`. ## Overriding Install If you don't want your blueprint to install the contents of `files` you can override the `install` method. It receives the same `options` object described above and must return a promise. See the built-in `resource` blueprint for an example of this. */ function Blueprint(blueprintPath) { this.path = blueprintPath; this.name = path.basename(blueprintPath); } Blueprint.__proto__ = require('./core-object'); Blueprint.prototype.constructor = Blueprint; /* @method files @return {Array} Contents of the blueprint's files directory */ Blueprint.prototype.files = function() { if (this._files) { return this._files; } return this._files = walkSync(path.join(this.path, 'files')); }; /* @method srcPath @param {String} file @return {String} Resolved path to the file */ Blueprint.prototype.srcPath = function(file) { return path.resolve(this.path, 'files', file); }; /* Hook for normalizing entity name @method normalizeEntityName @param {String} entityName @return {null} */ Blueprint.prototype.normalizeEntityName = function(entityName) { var trailingSlash = /(\/$|\\$)/; if(trailingSlash.test(entityName)) { throw new Error('You specified "' + entityName + '", but you can\'t use a ' + 'trailing slash as an entity name with generators. Please ' + 're-run the command with "' + entityName.replace(trailingSlash, '') + '".\n'); } return entityName; }; /* @method install @param {Object} options @return {Promise} */ Blueprint.prototype.install = function(options) { var ui = this.ui = options.ui; var intoDir = options.target; var dryRun = options.dryRun; var locals = this._locals(options); this.project = options.project; var actions = { write: function(info) { ui.write(' ' + chalk.green('create') + ' ' + info.displayPath + '\n'); if (!dryRun) { return writeFile(info.outputPath, info.render()); } }, skip: function(info) { var label = 'skip'; if (info.resolution === 'identical') { label = 'identical'; } ui.write(' ' + chalk.yellow(label) + ' ' + info.displayPath + '\n'); }, overwrite: function(info) { ui.write(' ' + chalk.yellow('overwrite') + ' ' + info.displayPath + '\n'); if (!dryRun) { return writeFile(info.outputPath, info.render()); } }, edit: function(info) { ui.write(' ' + chalk.green('edited') + ' ' + info.displayPath + '\n'); } }; function commit(result) { var action = actions[result.action]; if (action) { return action(result); } else { throw new Error('Tried to call action \"' + result.action + '\" but it does not exist'); } } ui.write('installing\n'); if (dryRun) { ui.write(chalk.yellow('You specified the dry-run flag, so no changes will be written.\n')); } if(options.entity) { options.entity.name = this.normalizeEntityName(options.entity.name); } return this.processFiles(intoDir, locals) .map(commit) .then(this.afterInstall.bind(this, options)); }; /* Hook for running operations after install. @method afterInstall @return {Promise|null} */ Blueprint.prototype.afterInstall = function() {}; /* Hook for adding additional locals @method locals @return {Object|null} */ Blueprint.prototype.locals = function() {}; /* @method buildFileInfo @param {Function} destPath @param {Object} templateVariables @param {String} file @return {FileInfo} */ Blueprint.prototype.buildFileInfo = function(destPath, templateVariables, file) { var mappedPath = this.mapFile(file, templateVariables); return new FileInfo({ action: 'write', outputPath: destPath(mappedPath), displayPath: mappedPath, inputPath: this.srcPath(file), templateVariables: templateVariables, ui: this.ui }); }; /* @method isUpdate @return {Boolean} */ Blueprint.prototype.isUpdate = function() { if (this.project && this.project.isEmberCLIProject) { return this.project.isEmberCLIProject(); } }; /* @method processFiles @param {String} intoDir @param {Object} templateVariables */ Blueprint.prototype.processFiles = function(intoDir, templateVariables) { function destPath(file) { return path.join(intoDir, file); } var fileInfos = this.files(). map(this.buildFileInfo.bind(this, destPath, templateVariables)); if (this.isUpdate()) { Blueprint.ignoredFiles = Blueprint.ignoredFiles.concat(Blueprint.ignoredUpdateFiles); } function isValidFile(fileInfo) { if (isIgnored(fileInfo)) { return Promise.resolve(false); } else { return isFile(fileInfo); } } return Promise.filter(fileInfos, isValidFile). map(prepareConfirm). then(function(infos) { infos.forEach(markIdenticalToBeSkipped); var infosNeedingConfirmation = infos.reduce(gatherConfirmationMessages, []); return sequence(infosNeedingConfirmation).returns(infos); }); }; /* @method mapFile @param {String} file @return {String} */ Blueprint.prototype.mapFile = function(file, locals) { file = Blueprint.renamedFiles[file] || file; return file.replace('__name__', locals.dasherizedModuleName); }; /* @private @method _locals @param {Object} options @return {Object} */ Blueprint.prototype._locals = function(options) { var packageName = options.project.name(); var moduleName = options.entity && options.entity.name || packageName; var sanitizedModuleName = moduleName.replace('/', '-'); var standardLocals = { dasherizedPackageName: stringUtils.dasherize(packageName), classifiedPackageName: stringUtils.classify(packageName), dasherizedModuleName: stringUtils.dasherize(moduleName), classifiedModuleName: stringUtils.classify(sanitizedModuleName), camelizedModuleName: stringUtils.camelize(sanitizedModuleName) }; var customLocals = this.locals(options); return merge({}, standardLocals, customLocals); }; /* @static @method lookup @namespace Blueprint @param {String} [name] @param {Object} [options] @param {Array} [options.paths] Extra paths to search for blueprints @param {Object} [options.properties] Properties @return {Blueprint} */ Blueprint.lookup = function(name, options) { options = options || {}; var lookupPaths = generateLookupPaths(options.paths); var lookupPath; var blueprintPath; var constructorPath; var blueprintModule; var Constructor; for (var i = 0; lookupPath = lookupPaths[i]; i++) { blueprintPath = path.resolve(lookupPath, name); if (!fs.existsSync(blueprintPath)) { continue; } constructorPath = path.resolve(blueprintPath, 'index.js'); if (fs.existsSync(constructorPath)) { blueprintModule = require(constructorPath); if (typeof blueprintModule === 'function') { Constructor = blueprintModule; } else { Constructor = Blueprint.extend(blueprintModule); } } else { Constructor = Blueprint; } return new Constructor(blueprintPath); } throw new Error('Unknown blueprint: ' + name); }; /* @static @method list @namespace Blueprint @param {Object} [options] @param {Array} [options.paths] Extra paths to search for blueprints @return {Blueprint} */ Blueprint.list = function(options) { options = options || {}; var lookupPaths = generateLookupPaths(options.paths); return lookupPaths.map(function(lookupPath) { var source = path.basename(path.join(lookupPath, '..')); var blueprints = glob.sync(path.join(lookupPath, '*')); blueprints = blueprints.map(function(blueprint) { return path.basename(blueprint); }); return { source: source, blueprints: blueprints }; }); }; /* @static @property renameFiles */ Blueprint.renamedFiles = { 'gitignore': '.gitignore' }; /* @static @property ignoredFiles */ Blueprint.ignoredFiles = [ '.DS_Store' ]; /* @static @property ignoredUpdateFiles */ Blueprint.ignoredUpdateFiles = [ '.gitkeep' ]; /* @static @property defaultLookupPaths */ Blueprint.defaultLookupPaths = function() { return [ path.resolve(__dirname, '..', '..', 'blueprints') ]; }; /* @private @method prepareConfirm @param {FileInfo} info @return {Promise} */ function prepareConfirm(info) { return info.checkForConflict().then(function(resolution) { info.resolution = resolution; return info; }); } /* @private @method markIdenticalToBeSkipped @param {FileInfo} info */ function markIdenticalToBeSkipped(info) { if (info.resolution === 'identical') { info.action = 'skip'; } } /* @private @method gatherConfirmationMessages @param {Array} collection @param {FileInfo} info @return {Array} */ function gatherConfirmationMessages(collection, info) { if (info.resolution === 'confirm') { collection.push(info.confirmOverwriteTask()); } return collection; } /* @private @method isFile @param {FileInfo} info @return {Boolean} */ function isFile(info) { return stat(info.inputPath).invoke('isFile'); } /* @private @method isIgnored @param {FileInfo} info @return {Boolean} */ function isIgnored(info) { var fn = info.inputPath; return any(Blueprint.ignoredFiles, function(ignoredFile) { return minimatch(fn, ignoredFile, { matchBase: true }); }); } /* Combines provided lookup paths with defaults and removes duplicates. @private @method generateLookupPaths @param {Array} lookupPaths @return {Array} */ function generateLookupPaths(lookupPaths) { lookupPaths = lookupPaths || []; lookupPaths = lookupPaths.concat(Blueprint.defaultLookupPaths()); return uniq(lookupPaths); }