123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424 |
- var logger = process.logging || require('../util/log');
- var log = logger('generators:action');
- var fs = require('fs');
- var path = require('path');
- var events = require('events');
- var mkdirp = require('mkdirp');
- var isBinaryFile = require('isbinaryfile');
- var rimraf = require('rimraf');
- var async = require('async');
- var iconv = require('iconv-lite');
- var chalk = require('chalk');
- var actions = module.exports;
- actions.log = log;
- /**
- * Stores and return the source root for this class. The source root is used to
- * prefix filepath with `.read()` or `.template()`.
- *
- * @param {String} root
- */
- actions.sourceRoot = function sourceRoot(root) {
- if (root) {
- this._sourceRoot = path.resolve(root);
- }
- return this._sourceRoot;
- };
- /**
- * Sets the destination root for this class, the working directory. Relative
- * path are added to the directory where the script was invoked and
- * expanded.
- *
- * This automatically creates the working directory if it doensn't exists and
- * `cd` into it.
- *
- * @param {String} root
- */
- actions.destinationRoot = function destinationRoot(root) {
- if (root) {
- this._destinationRoot = path.resolve(root);
- if (!fs.existsSync(root)) {
- this.mkdir(root);
- }
- process.chdir(root);
- }
- return this._destinationRoot || './';
- };
- /**
- * 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() {
- // we follow XDG specs if possible:
- // http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
- if (process.env.XDG_CACHE_HOME) {
- return path.join(process.env.XDG_CACHE_HOME, 'yeoman');
- }
- // otherwise, we fallback to a temp dir in the home
- var home = process.env[process.platform === 'win32' ? 'USERPROFILE' : 'HOME'];
- return path.join(home, '.cache/yeoman');
- };
- /**
- * 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
- * @param {String} destination
- * @param {Function} process
- */
- actions.copy = function copy(source, destination, process) {
- var body;
- destination = destination || source;
- if (typeof destination === 'function') {
- process = destination;
- destination = source;
- }
- source = this.isPathAbsolute(source) ? source : path.join(this.sourceRoot(), source);
- var encoding = null;
- var binary = isBinaryFile(source);
- if (!binary) {
- encoding = 'utf8';
- }
- body = fs.readFileSync(source, encoding);
- if (typeof process === 'function' && !binary) {
- body = process(body, source, destination, {
- encoding: encoding
- });
- }
- try {
- body = this.engine(body, this);
- } catch (err) {
- // this happens in some cases when trying to copy a JS file like lodash/underscore
- // (conflicting the templating engine)
- }
- this.checkForCollision(destination, body, function (err, config) {
- var stats;
- if (err) {
- config.callback(err);
- return this.emit('error', err);
- }
- // create or force means file write, identical or skip prevent the
- // actual write.
- if (!(/force|create/.test(config.status))) {
- return config.callback();
- }
- mkdirp.sync(path.dirname(destination));
- fs.writeFileSync(destination, body);
- // synchronize stats and modification times from the original file.
- stats = fs.statSync(source);
- try {
- fs.chmodSync(destination, stats.mode);
- fs.utimesSync(destination, stats.atime, stats.mtime);
- } catch (err) {
- this.log.error('Error setting permissions of "' + chalk.bold(destination) + '" file: ' + err);
- }
- config.callback();
- }.bind(this));
- return this;
- };
- /**
- * A simple method to read the content of the 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
- */
- actions.read = function read(filepath, encoding) {
- var contents;
- if (!this.isPathAbsolute(filepath)) {
- filepath = path.join(this.sourceRoot(), filepath);
- }
- try {
- contents = fs.readFileSync(String(filepath));
- // if encoding is not explicitly null, convert from encoded buffer to a
- // string. if no encoding was specified, use the default.
- if (encoding !== null) {
- contents = iconv.decode(contents, encoding || 'utf8');
- // strip any BOM that might exist.
- if (contents.charCodeAt(0) === 0xFEFF) {
- contents = contents.substring(1);
- }
- }
- return contents;
- } catch (e) {
- throw new Error('Unable to read "' + filepath + '" file (Error code: ' + e.code + ').');
- }
- };
- /**
- * Writes a chunk of data to a given `filepath`, checking for collision prior
- * to the file write.
- *
- * @param {String} filepath
- * @param {String} content
- */
- actions.write = function write(filepath, content) {
- this.checkForCollision(filepath, content, function (err, config) {
- if (err) {
- config.callback(err);
- return this.emit('error', err);
- }
- // create or force means file write, identical or skip prevent the
- // actual write
- if (/force|create/.test(config.status)) {
- mkdirp.sync(path.dirname(filepath));
- fs.writeFileSync(filepath, content);
- }
- config.callback();
- });
- return this;
- };
- /**
- * File collision checked. Takes a `filepath` (the file about to be written)
- * and the actual content. A basic check is done to see if the file exists, if
- * it does:
- *
- * 1. Read its content from `fs`
- * 2. Compare it with the provided content
- * 3. If identical, mark it as is and skip the check
- * 4. If diverged, prepare and show up the file collision menu
- *
- * The menu has the following options:
- *
- * - `Y` Yes, overwrite
- * - `n` No, do not overwrite
- * - `a` All, overwrite this and all others
- * - `q` Quit, abort
- * - `d` Diff, show the differences between the old and the new
- * - `h` Help, show this help
- *
- * @param {String} filepath
- * @param {String} content
- * @param {Function} cb
- */
- actions.checkForCollision = function checkForCollision(filepath, content, cb) {
- this.conflicter.add({
- file: filepath,
- content: content
- });
- this.conflicter.once('resolved:' + filepath, cb.bind(this, null));
- };
- /**
- * 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`. `data` is an optional hash to pass to the template, if
- * undefined, executes the template in the generator instance context.
- *
- * @param {String} source
- * @param {String} destination
- * @param {Object} data
- */
- actions.template = function template(source, destination, data) {
- data = data || this;
- destination = destination || source;
- var body = this.read(source, 'utf8');
- body = this.engine(body, data);
- this.write(destination, 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`.
- *
- * @param {String} body
- * @param {Object} data
- */
- actions.engine = function engine(body, data) {
- if (!this._engine) {
- throw new Error('Trying to render template without valid engine.');
- }
- return this._engine.detect && this._engine.detect(body) ?
- this._engine(body, data) :
- body;
- };
- /**
- * Copies recursively the files from source directory to root directory.
- *
- * @param {String} source
- * @param {String} destination
- * @param {Function} process
- */
- actions.directory = function directory(source, destination, process) {
- var root = path.join(this.sourceRoot(), source);
- var files = this.expandFiles('**', { dot: true, cwd: root });
- var self = this;
- destination = destination || source;
- if (typeof destination === 'function') {
- process = destination;
- destination = source;
- }
- // get the path relative to the template root, and copy to the relative destination
- var resolveFiles = function (filepath) {
- return function (next) {
- if (!filepath) {
- self.emit('directory:end');
- return next();
- }
- var dest = path.join(destination, filepath);
- self.copy(path.join(root, filepath), dest, process);
- return next();
- };
- };
- async.parallel(files.map(resolveFiles));
- return this;
- };
- /**
- * Remotely fetch a package on github, store this into a _cache folder, and
- * provide a "remote" object as a facade API to ourself (part of genrator API,
- * copy, template, directory). It's possible to remove local cache, and force
- * a new remote fetch of the package on Github.
- *
- * ### Examples:
- *
- * this.remote('user', 'repo', function(err, remote) {
- * remote.copy('.', 'vendors/user-repo');
- * });
- *
- * @param {String} username
- * @param {String} repo
- * @param {String} branch
- * @param {Function} cb
- * @param {Boolean} refresh
- */
- actions.remote = function (username, repo, branch, cb, refresh) {
- if (!cb) {
- cb = branch;
- branch = 'master';
- }
- var self = this;
- var cache = path.join(this.cacheRoot(), username, repo, branch);
- var url = 'http://github.com/' + [username, repo, 'archive', branch].join('/') + '.tar.gz';
- fs.stat(cache, function (err) {
- // already cached
- if (!err) {
- // no refresh, so we can use this cache
- if (!refresh) {
- return done();
- }
- // otherwise, we need to remove it, to fetch it again
- rimraf(cache, function (err) {
- if (err) {
- return cb(err);
- }
- self.tarball(url, cache, done);
- });
- } else {
- self.tarball(url, cache, done);
- }
- });
- function done(err) {
- if (err) {
- return cb(err);
- }
- var files = self.expandFiles('**', { cwd: cache, dot: true });
- var remote = {};
- remote.cachePath = cache;
- // simple proxy to `.copy(source, destination)`
- remote.copy = function copy(source, destination) {
- source = path.join(cache, source);
- self.copy(source, destination);
- return this;
- };
- // same as `.template(source, destination, data)`
- remote.template = function template(source, destination, data) {
- data = data || self;
- destination = destination || source;
- source = path.join(cache, source);
- var body = self.engine(self.read(source), data);
- self.write(destination, body);
- };
- // same as `.template(source, destination)`
- remote.directory = function directory(source, destination) {
- var root = self.sourceRoot();
- self.sourceRoot(cache);
- self.directory(source, destination);
- self.sourceRoot(root);
- };
- cb(err, remote, files);
- }
- return this;
- };
|