UNPKG

yeoman-generator

Version:

Rails-inspired generator system that provides scaffolding for your apps

297 lines (248 loc) 9.46 kB
'use strict'; var fs = require('fs'); var path = require('path'); var mkdirp = require('mkdirp'); var istextorbinary = require('istextorbinary'); var chalk = require('chalk'); var xdgBasedir = require('xdg-basedir'); var glob = require('glob'); var engine = require('../util/engines').underscore; function isPathAbsolute() { var filepath = path.join.apply(path, arguments); return path.resolve(filepath) === filepath; } /** * @mixin * @alias actions/actions */ var actions = module.exports; /** * Stores and return the cache root for this class. The cache root is used to * `git clone` repositories from github by `.remote()` for example. */ actions.cacheRoot = function cacheRoot() { return path.join(xdgBasedir.cache, 'yeoman'); }; // Copy helper for two versions of copy action actions._prepCopy = function _prepCopy(source, destination, process) { destination = destination || source; if (typeof destination === 'function') { process = destination; destination = source; } source = isPathAbsolute(source) ? source : path.join(this.sourceRoot(), source); destination = isPathAbsolute(destination) ? destination : path.join(this.destinationRoot(), destination); var encoding = null; var body = fs.readFileSync(source); var isText = istextorbinary.isTextSync(undefined, body); if (isText) { body = body.toString(); if (typeof process === 'function') { body = process(body, source, destination, { encoding: encoding }); } } return { body: body, encoding: encoding, destination: destination, source: source }; }; /** * Make some of the file API aware of our source/destination root paths. * `copy`, `template` (only when could be applied/required by legacy code), * `write` and alike consider. * * @param {String} source Source file to copy from. Relative to this.sourceRoot() * @param {String} destination Destination file to write to. Relative to this.destinationRoot() * @param {Function} process */ actions.copy = function copy(source, destination, process) { var file = this._prepCopy(source, destination, process); try { file.body = this.engine(file.body, this); } catch (err) { // this happens in some cases when trying to copy a JS file like lodash/underscore // (conflicting the templating engine) } this.fs.copy(file.source, file.destination, { process: function () { return file.body; } }); return this; }; /** * Bulk copy * https://github.com/yeoman/generator/pull/359 * https://github.com/yeoman/generator/issues/350 * * A copy method skipping templating and conflict checking. It will allow copying * a large amount of files without causing too many recursion errors. You should * never use this method, unless there's no other solution. * * @param {String} source Source file to copy from. Relative to this.sourceRoot() * @param {String} destination Destination file to write to. Relative to this.destinationRoot() * @param {Function} process */ actions.bulkCopy = function bulkCopy(source, destination, process) { var file = this._prepCopy(source, destination, process); mkdirp.sync(path.dirname(file.destination)); fs.writeFileSync(file.destination, file.body); // synchronize stats and modification times from the original file. var stats = fs.statSync(file.source); try { fs.chmodSync(file.destination, stats.mode); fs.utimesSync(file.destination, stats.atime, stats.mtime); } catch (err) { this.log.error('Error setting permissions of "' + chalk.bold(file.destination) + '" file: ' + err); } this.log.create(file.destination); return this; }; /** * A simple method to read the content of a file borrowed from Grunt: * https://github.com/gruntjs/grunt/blob/master/lib/grunt/file.js * * Discussion and future plans: * https://github.com/yeoman/generator/pull/220 * * The encoding is `utf8` by default, to read binary files, pass the proper * encoding or null. Non absolute path are prefixed by the source root. * * @param {String} filepath * @param {String} [encoding="utf-8"] Character encoding. */ actions.read = function read(filepath, encoding) { if (!isPathAbsolute(filepath)) { filepath = path.join(this.sourceRoot(), filepath); } var contents = this.fs.read(filepath, { raw: true }); return contents.toString(encoding || 'utf8'); }; /** * Writes a chunk of data to a given `filepath`, checking for collision prior * to the file write. * * @param {String} filepath * @param {String} content * @param {Object} writeFile An object containing options for the file writing, as shown here: http://nodejs.org/api/fs.html#fs_fs_writefile_filename_data_options_callback */ actions.write = function write(filepath, content, writeFile) { this.fs.write(filepath, content); return this; }; /** * Gets a template at the relative source, executes it and makes a copy * at the relative destination. If the destination is not given it's assumed * to be equal to the source relative to destination. * * Use configured engine to render the provided `source` template at the given * `destination`. The `destination` path is a template itself and supports variable * interpolation. `data` is an optional hash to pass to the template, if undefined, * executes the template in the generator instance context. * * use options to pass parameters to engine (like _.templateSettings) * * @param {String} source Source file to read from. Relative to this.sourceRoot() * @param {String} destination Destination file to write to. Relative to this.destinationRoot(). * @param {Object} data Hash to pass to the template. Leave undefined to use the generator instance context. * @param {Object} options */ actions.template = function template(source, destination, data, options) { if (!destination || !isPathAbsolute(destination)) { destination = path.join( this.destinationRoot(), this.engine(destination || source, data || this, options) ); } if (!isPathAbsolute(source)) { source = path.join( this.sourceRoot(), this.engine(source, data || this, options) ); } var body = this.engine(this.fs.read(source), data || this, options); // Using copy to keep the file mode of the previous file this.fs.copy(source, destination, { process: function () { return body; } }); return this; }; /** * The engine method is the function used whenever a template needs to be rendered. * * It uses the configured engine (default: underscore) to render the `body` * template with the provided `data`. * * use options to pass paramters to engine (like _.templateSettings) * * @param {String} body * @param {Object} data * @param {Object} options */ actions.engine = function (body, data, options) { return engine.detect(body) ? engine(body, data, options) : body; }; // Shared directory method actions._directory = function _directory(source, destination, process, bulk) { // Only add sourceRoot if the path is not absolute var root = isPathAbsolute(source) ? source : path.join(this.sourceRoot(), source); var files = glob.sync('**', { dot: true, nodir: true, cwd: root }); destination = destination || source; if (typeof destination === 'function') { process = destination; destination = source; } var cp = this.copy; if (bulk) { cp = this.bulkCopy; } // get the path relative to the template root, and copy to the relative destination for (var i in files) { var dest = path.join(destination, files[i]); cp.call(this, path.join(root, files[i]), dest, process); } return this; }; /** * Copies recursively the files from source directory to root directory. * * @param {String} source Source directory to copy from. Relative to this.sourceRoot() * @param {String} destination Directory to copy the source files into. Relative to this.destinationRoot(). * @param {Function} process Receive in order: the body, the source path, the destination * path and a list of options containing the encoding. It should * return the new body. */ actions.directory = function directory(source, destination, process) { return this._directory(source, destination, process); }; /** * Copies recursively the files from source directory to root directory. * * A copy method skiping templating and conflict checking. It will allow copying * a large amount of files without causing too much recursion errors. You should * never use this method, unless there's no other solution. * * @param {String} source Source directory to copy from. Relative to this.sourceRoot() * @param {String} destination Directory to copy the source files into.Relative to this.destinationRoot(). * @param {Function} process */ actions.bulkDirectory = function directory(source, destination, process) { // Join the source here because the conflicter will not run // until next tick, which resets the source root on remote // bulkCopy operations source = path.join(this.sourceRoot(), source); this.conflicter.checkForCollision(destination, null, function (err, status) { // create or force means file write, identical or skip prevent the // actual write. if (/force|create/.test(status)) { this._directory(source, destination, process, true); } }.bind(this)); return this; };