actions.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. var logger = process.logging || require('../util/log');
  2. var log = logger('generators:action');
  3. var fs = require('fs');
  4. var path = require('path');
  5. var events = require('events');
  6. var mkdirp = require('mkdirp');
  7. var isBinaryFile = require('isbinaryfile');
  8. var rimraf = require('rimraf');
  9. var async = require('async');
  10. var iconv = require('iconv-lite');
  11. var chalk = require('chalk');
  12. var actions = module.exports;
  13. actions.log = log;
  14. /**
  15. * Stores and return the source root for this class. The source root is used to
  16. * prefix filepath with `.read()` or `.template()`.
  17. *
  18. * @param {String} root
  19. */
  20. actions.sourceRoot = function sourceRoot(root) {
  21. if (root) {
  22. this._sourceRoot = path.resolve(root);
  23. }
  24. return this._sourceRoot;
  25. };
  26. /**
  27. * Sets the destination root for this class, the working directory. Relative
  28. * path are added to the directory where the script was invoked and
  29. * expanded.
  30. *
  31. * This automatically creates the working directory if it doensn't exists and
  32. * `cd` into it.
  33. *
  34. * @param {String} root
  35. */
  36. actions.destinationRoot = function destinationRoot(root) {
  37. if (root) {
  38. this._destinationRoot = path.resolve(root);
  39. if (!fs.existsSync(root)) {
  40. this.mkdir(root);
  41. }
  42. process.chdir(root);
  43. }
  44. return this._destinationRoot || './';
  45. };
  46. /**
  47. * Stores and return the cache root for this class. The cache root is used to
  48. * `git clone` repositories from github by `.remote()` for example.
  49. */
  50. actions.cacheRoot = function cacheRoot() {
  51. // we follow XDG specs if possible:
  52. // http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
  53. if (process.env.XDG_CACHE_HOME) {
  54. return path.join(process.env.XDG_CACHE_HOME, 'yeoman');
  55. }
  56. // otherwise, we fallback to a temp dir in the home
  57. var home = process.env[process.platform === 'win32' ? 'USERPROFILE' : 'HOME'];
  58. return path.join(home, '.cache/yeoman');
  59. };
  60. /**
  61. * Make some of the file API aware of our source/destination root paths.
  62. * `copy`, `template` (only when could be applied/required by legacy code),
  63. * `write` and alike consider.
  64. *
  65. * @param {String} source
  66. * @param {String} destination
  67. * @param {Function} process
  68. */
  69. actions.copy = function copy(source, destination, process) {
  70. var body;
  71. destination = destination || source;
  72. if (typeof destination === 'function') {
  73. process = destination;
  74. destination = source;
  75. }
  76. source = this.isPathAbsolute(source) ? source : path.join(this.sourceRoot(), source);
  77. var encoding = null;
  78. var binary = isBinaryFile(source);
  79. if (!binary) {
  80. encoding = 'utf8';
  81. }
  82. body = fs.readFileSync(source, encoding);
  83. if (typeof process === 'function' && !binary) {
  84. body = process(body, source, destination, {
  85. encoding: encoding
  86. });
  87. }
  88. try {
  89. body = this.engine(body, this);
  90. } catch (err) {
  91. // this happens in some cases when trying to copy a JS file like lodash/underscore
  92. // (conflicting the templating engine)
  93. }
  94. this.checkForCollision(destination, body, function (err, config) {
  95. var stats;
  96. if (err) {
  97. config.callback(err);
  98. return this.emit('error', err);
  99. }
  100. // create or force means file write, identical or skip prevent the
  101. // actual write.
  102. if (!(/force|create/.test(config.status))) {
  103. return config.callback();
  104. }
  105. mkdirp.sync(path.dirname(destination));
  106. fs.writeFileSync(destination, body);
  107. // synchronize stats and modification times from the original file.
  108. stats = fs.statSync(source);
  109. try {
  110. fs.chmodSync(destination, stats.mode);
  111. fs.utimesSync(destination, stats.atime, stats.mtime);
  112. } catch (err) {
  113. this.log.error('Error setting permissions of "' + chalk.bold(destination) + '" file: ' + err);
  114. }
  115. config.callback();
  116. }.bind(this));
  117. return this;
  118. };
  119. /**
  120. * A simple method to read the content of the a file borrowed from Grunt:
  121. * https://github.com/gruntjs/grunt/blob/master/lib/grunt/file.js
  122. *
  123. * Discussion and future plans:
  124. * https://github.com/yeoman/generator/pull/220
  125. *
  126. * The encoding is `utf8` by default, to read binary files, pass the proper
  127. * encoding or null. Non absolute path are prefixed by the source root.
  128. *
  129. * @param {String} filepath
  130. * @param {String} encoding
  131. */
  132. actions.read = function read(filepath, encoding) {
  133. var contents;
  134. if (!this.isPathAbsolute(filepath)) {
  135. filepath = path.join(this.sourceRoot(), filepath);
  136. }
  137. try {
  138. contents = fs.readFileSync(String(filepath));
  139. // if encoding is not explicitly null, convert from encoded buffer to a
  140. // string. if no encoding was specified, use the default.
  141. if (encoding !== null) {
  142. contents = iconv.decode(contents, encoding || 'utf8');
  143. // strip any BOM that might exist.
  144. if (contents.charCodeAt(0) === 0xFEFF) {
  145. contents = contents.substring(1);
  146. }
  147. }
  148. return contents;
  149. } catch (e) {
  150. throw new Error('Unable to read "' + filepath + '" file (Error code: ' + e.code + ').');
  151. }
  152. };
  153. /**
  154. * Writes a chunk of data to a given `filepath`, checking for collision prior
  155. * to the file write.
  156. *
  157. * @param {String} filepath
  158. * @param {String} content
  159. */
  160. actions.write = function write(filepath, content) {
  161. this.checkForCollision(filepath, content, function (err, config) {
  162. if (err) {
  163. config.callback(err);
  164. return this.emit('error', err);
  165. }
  166. // create or force means file write, identical or skip prevent the
  167. // actual write
  168. if (/force|create/.test(config.status)) {
  169. mkdirp.sync(path.dirname(filepath));
  170. fs.writeFileSync(filepath, content);
  171. }
  172. config.callback();
  173. });
  174. return this;
  175. };
  176. /**
  177. * File collision checked. Takes a `filepath` (the file about to be written)
  178. * and the actual content. A basic check is done to see if the file exists, if
  179. * it does:
  180. *
  181. * 1. Read its content from `fs`
  182. * 2. Compare it with the provided content
  183. * 3. If identical, mark it as is and skip the check
  184. * 4. If diverged, prepare and show up the file collision menu
  185. *
  186. * The menu has the following options:
  187. *
  188. * - `Y` Yes, overwrite
  189. * - `n` No, do not overwrite
  190. * - `a` All, overwrite this and all others
  191. * - `q` Quit, abort
  192. * - `d` Diff, show the differences between the old and the new
  193. * - `h` Help, show this help
  194. *
  195. * @param {String} filepath
  196. * @param {String} content
  197. * @param {Function} cb
  198. */
  199. actions.checkForCollision = function checkForCollision(filepath, content, cb) {
  200. this.conflicter.add({
  201. file: filepath,
  202. content: content
  203. });
  204. this.conflicter.once('resolved:' + filepath, cb.bind(this, null));
  205. };
  206. /**
  207. * Gets a template at the relative source, executes it and makes a copy
  208. * at the relative destination. If the destination is not given it's assumed
  209. * to be equal to the source relative to destination.
  210. *
  211. * Use configured engine to render the provided `source` template at the given
  212. * `destination`. `data` is an optional hash to pass to the template, if
  213. * undefined, executes the template in the generator instance context.
  214. *
  215. * @param {String} source
  216. * @param {String} destination
  217. * @param {Object} data
  218. */
  219. actions.template = function template(source, destination, data) {
  220. data = data || this;
  221. destination = destination || source;
  222. var body = this.read(source, 'utf8');
  223. body = this.engine(body, data);
  224. this.write(destination, body);
  225. return this;
  226. };
  227. /**
  228. * The engine method is the function used whenever a template needs to be rendered.
  229. *
  230. * It uses the configured engine (default: underscore) to render the `body`
  231. * template with the provided `data`.
  232. *
  233. * @param {String} body
  234. * @param {Object} data
  235. */
  236. actions.engine = function engine(body, data) {
  237. if (!this._engine) {
  238. throw new Error('Trying to render template without valid engine.');
  239. }
  240. return this._engine.detect && this._engine.detect(body) ?
  241. this._engine(body, data) :
  242. body;
  243. };
  244. /**
  245. * Copies recursively the files from source directory to root directory.
  246. *
  247. * @param {String} source
  248. * @param {String} destination
  249. * @param {Function} process
  250. */
  251. actions.directory = function directory(source, destination, process) {
  252. var root = path.join(this.sourceRoot(), source);
  253. var files = this.expandFiles('**', { dot: true, cwd: root });
  254. var self = this;
  255. destination = destination || source;
  256. if (typeof destination === 'function') {
  257. process = destination;
  258. destination = source;
  259. }
  260. // get the path relative to the template root, and copy to the relative destination
  261. var resolveFiles = function (filepath) {
  262. return function (next) {
  263. if (!filepath) {
  264. self.emit('directory:end');
  265. return next();
  266. }
  267. var dest = path.join(destination, filepath);
  268. self.copy(path.join(root, filepath), dest, process);
  269. return next();
  270. };
  271. };
  272. async.parallel(files.map(resolveFiles));
  273. return this;
  274. };
  275. /**
  276. * Remotely fetch a package on github, store this into a _cache folder, and
  277. * provide a "remote" object as a facade API to ourself (part of genrator API,
  278. * copy, template, directory). It's possible to remove local cache, and force
  279. * a new remote fetch of the package on Github.
  280. *
  281. * ### Examples:
  282. *
  283. * this.remote('user', 'repo', function(err, remote) {
  284. * remote.copy('.', 'vendors/user-repo');
  285. * });
  286. *
  287. * @param {String} username
  288. * @param {String} repo
  289. * @param {String} branch
  290. * @param {Function} cb
  291. * @param {Boolean} refresh
  292. */
  293. actions.remote = function (username, repo, branch, cb, refresh) {
  294. if (!cb) {
  295. cb = branch;
  296. branch = 'master';
  297. }
  298. var self = this;
  299. var cache = path.join(this.cacheRoot(), username, repo, branch);
  300. var url = 'http://github.com/' + [username, repo, 'archive', branch].join('/') + '.tar.gz';
  301. fs.stat(cache, function (err) {
  302. // already cached
  303. if (!err) {
  304. // no refresh, so we can use this cache
  305. if (!refresh) {
  306. return done();
  307. }
  308. // otherwise, we need to remove it, to fetch it again
  309. rimraf(cache, function (err) {
  310. if (err) {
  311. return cb(err);
  312. }
  313. self.tarball(url, cache, done);
  314. });
  315. } else {
  316. self.tarball(url, cache, done);
  317. }
  318. });
  319. function done(err) {
  320. if (err) {
  321. return cb(err);
  322. }
  323. var files = self.expandFiles('**', { cwd: cache, dot: true });
  324. var remote = {};
  325. remote.cachePath = cache;
  326. // simple proxy to `.copy(source, destination)`
  327. remote.copy = function copy(source, destination) {
  328. source = path.join(cache, source);
  329. self.copy(source, destination);
  330. return this;
  331. };
  332. // same as `.template(source, destination, data)`
  333. remote.template = function template(source, destination, data) {
  334. data = data || self;
  335. destination = destination || source;
  336. source = path.join(cache, source);
  337. var body = self.engine(self.read(source), data);
  338. self.write(destination, body);
  339. };
  340. // same as `.template(source, destination)`
  341. remote.directory = function directory(source, destination) {
  342. var root = self.sourceRoot();
  343. self.sourceRoot(cache);
  344. self.directory(source, destination);
  345. self.sourceRoot(root);
  346. };
  347. cb(err, remote, files);
  348. }
  349. return this;
  350. };