No Description

file.js 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  1. /*
  2. * grunt
  3. * http://gruntjs.com/
  4. *
  5. * Copyright (c) 2014 "Cowboy" Ben Alman
  6. * Licensed under the MIT license.
  7. * https://github.com/gruntjs/grunt/blob/master/LICENSE-MIT
  8. */
  9. 'use strict';
  10. var grunt = require('../grunt');
  11. // Nodejs libs.
  12. var fs = require('fs');
  13. var path = require('path');
  14. // The module to be exported.
  15. var file = module.exports = {};
  16. // External libs.
  17. file.glob = require('glob');
  18. file.minimatch = require('minimatch');
  19. file.findup = require('findup-sync');
  20. var YAML = require('js-yaml');
  21. var rimraf = require('rimraf');
  22. var iconv = require('iconv-lite');
  23. // Windows?
  24. var win32 = process.platform === 'win32';
  25. // Normalize \\ paths to / paths.
  26. var unixifyPath = function(filepath) {
  27. if (win32) {
  28. return filepath.replace(/\\/g, '/');
  29. } else {
  30. return filepath;
  31. }
  32. };
  33. // Change the current base path (ie, CWD) to the specified path.
  34. file.setBase = function() {
  35. var dirpath = path.join.apply(path, arguments);
  36. process.chdir(dirpath);
  37. };
  38. // Process specified wildcard glob patterns or filenames against a
  39. // callback, excluding and uniquing files in the result set.
  40. var processPatterns = function(patterns, fn) {
  41. // Filepaths to return.
  42. var result = [];
  43. // Iterate over flattened patterns array.
  44. grunt.util._.flatten(patterns).forEach(function(pattern) {
  45. // If the first character is ! it should be omitted
  46. var exclusion = pattern.indexOf('!') === 0;
  47. // If the pattern is an exclusion, remove the !
  48. if (exclusion) { pattern = pattern.slice(1); }
  49. // Find all matching files for this pattern.
  50. var matches = fn(pattern);
  51. if (exclusion) {
  52. // If an exclusion, remove matching files.
  53. result = grunt.util._.difference(result, matches);
  54. } else {
  55. // Otherwise add matching files.
  56. result = grunt.util._.union(result, matches);
  57. }
  58. });
  59. return result;
  60. };
  61. // Match a filepath or filepaths against one or more wildcard patterns. Returns
  62. // all matching filepaths.
  63. file.match = function(options, patterns, filepaths) {
  64. if (grunt.util.kindOf(options) !== 'object') {
  65. filepaths = patterns;
  66. patterns = options;
  67. options = {};
  68. }
  69. // Return empty set if either patterns or filepaths was omitted.
  70. if (patterns == null || filepaths == null) { return []; }
  71. // Normalize patterns and filepaths to arrays.
  72. if (!Array.isArray(patterns)) { patterns = [patterns]; }
  73. if (!Array.isArray(filepaths)) { filepaths = [filepaths]; }
  74. // Return empty set if there are no patterns or filepaths.
  75. if (patterns.length === 0 || filepaths.length === 0) { return []; }
  76. // Return all matching filepaths.
  77. return processPatterns(patterns, function(pattern) {
  78. return file.minimatch.match(filepaths, pattern, options);
  79. });
  80. };
  81. // Match a filepath or filepaths against one or more wildcard patterns. Returns
  82. // true if any of the patterns match.
  83. file.isMatch = function() {
  84. return file.match.apply(file, arguments).length > 0;
  85. };
  86. // Return an array of all file paths that match the given wildcard patterns.
  87. file.expand = function() {
  88. var args = grunt.util.toArray(arguments);
  89. // If the first argument is an options object, save those options to pass
  90. // into the file.glob.sync method.
  91. var options = grunt.util.kindOf(args[0]) === 'object' ? args.shift() : {};
  92. // Use the first argument if it's an Array, otherwise convert the arguments
  93. // object to an array and use that.
  94. var patterns = Array.isArray(args[0]) ? args[0] : args;
  95. // Return empty set if there are no patterns or filepaths.
  96. if (patterns.length === 0) { return []; }
  97. // Return all matching filepaths.
  98. var matches = processPatterns(patterns, function(pattern) {
  99. // Find all matching files for this pattern.
  100. return file.glob.sync(pattern, options);
  101. });
  102. // Filter result set?
  103. if (options.filter) {
  104. matches = matches.filter(function(filepath) {
  105. filepath = path.join(options.cwd || '', filepath);
  106. try {
  107. if (typeof options.filter === 'function') {
  108. return options.filter(filepath);
  109. } else {
  110. // If the file is of the right type and exists, this should work.
  111. return fs.statSync(filepath)[options.filter]();
  112. }
  113. } catch(e) {
  114. // Otherwise, it's probably not the right type.
  115. return false;
  116. }
  117. });
  118. }
  119. return matches;
  120. };
  121. var pathSeparatorRe = /[\/\\]/g;
  122. // The "ext" option refers to either everything after the first dot (default)
  123. // or everything after the last dot.
  124. var extDotRe = {
  125. first: /(\.[^\/]*)?$/,
  126. last: /(\.[^\/\.]*)?$/,
  127. };
  128. // Build a multi task "files" object dynamically.
  129. file.expandMapping = function(patterns, destBase, options) {
  130. options = grunt.util._.defaults({}, options, {
  131. extDot: 'first',
  132. rename: function(destBase, destPath) {
  133. return path.join(destBase || '', destPath);
  134. }
  135. });
  136. var files = [];
  137. var fileByDest = {};
  138. // Find all files matching pattern, using passed-in options.
  139. file.expand(options, patterns).forEach(function(src) {
  140. var destPath = src;
  141. // Flatten?
  142. if (options.flatten) {
  143. destPath = path.basename(destPath);
  144. }
  145. // Change the extension?
  146. if ('ext' in options) {
  147. destPath = destPath.replace(extDotRe[options.extDot], options.ext);
  148. }
  149. // Generate destination filename.
  150. var dest = options.rename(destBase, destPath, options);
  151. // Prepend cwd to src path if necessary.
  152. if (options.cwd) { src = path.join(options.cwd, src); }
  153. // Normalize filepaths to be unix-style.
  154. dest = dest.replace(pathSeparatorRe, '/');
  155. src = src.replace(pathSeparatorRe, '/');
  156. // Map correct src path to dest path.
  157. if (fileByDest[dest]) {
  158. // If dest already exists, push this src onto that dest's src array.
  159. fileByDest[dest].src.push(src);
  160. } else {
  161. // Otherwise create a new src-dest file mapping object.
  162. files.push({
  163. src: [src],
  164. dest: dest,
  165. });
  166. // And store a reference for later use.
  167. fileByDest[dest] = files[files.length - 1];
  168. }
  169. });
  170. return files;
  171. };
  172. // Like mkdir -p. Create a directory and any intermediary directories.
  173. file.mkdir = function(dirpath, mode) {
  174. if (grunt.option('no-write')) { return; }
  175. // Set directory mode in a strict-mode-friendly way.
  176. if (mode == null) {
  177. mode = parseInt('0777', 8) & (~process.umask());
  178. }
  179. dirpath.split(pathSeparatorRe).reduce(function(parts, part) {
  180. parts += part + '/';
  181. var subpath = path.resolve(parts);
  182. if (!file.exists(subpath)) {
  183. try {
  184. fs.mkdirSync(subpath, mode);
  185. } catch(e) {
  186. throw grunt.util.error('Unable to create directory "' + subpath + '" (Error code: ' + e.code + ').', e);
  187. }
  188. }
  189. return parts;
  190. }, '');
  191. };
  192. // Recurse into a directory, executing callback for each file.
  193. file.recurse = function recurse(rootdir, callback, subdir) {
  194. var abspath = subdir ? path.join(rootdir, subdir) : rootdir;
  195. fs.readdirSync(abspath).forEach(function(filename) {
  196. var filepath = path.join(abspath, filename);
  197. if (fs.statSync(filepath).isDirectory()) {
  198. recurse(rootdir, callback, unixifyPath(path.join(subdir || '', filename || '')));
  199. } else {
  200. callback(unixifyPath(filepath), rootdir, subdir, filename);
  201. }
  202. });
  203. };
  204. // The default file encoding to use.
  205. file.defaultEncoding = 'utf8';
  206. // Whether to preserve the BOM on file.read rather than strip it.
  207. file.preserveBOM = false;
  208. // Read a file, return its contents.
  209. file.read = function(filepath, options) {
  210. if (!options) { options = {}; }
  211. var contents;
  212. grunt.verbose.write('Reading ' + filepath + '...');
  213. try {
  214. contents = fs.readFileSync(String(filepath));
  215. // If encoding is not explicitly null, convert from encoded buffer to a
  216. // string. If no encoding was specified, use the default.
  217. if (options.encoding !== null) {
  218. contents = iconv.decode(contents, options.encoding || file.defaultEncoding);
  219. // Strip any BOM that might exist.
  220. if (!file.preserveBOM && contents.charCodeAt(0) === 0xFEFF) {
  221. contents = contents.substring(1);
  222. }
  223. }
  224. grunt.verbose.ok();
  225. return contents;
  226. } catch(e) {
  227. grunt.verbose.error();
  228. throw grunt.util.error('Unable to read "' + filepath + '" file (Error code: ' + e.code + ').', e);
  229. }
  230. };
  231. // Read a file, parse its contents, return an object.
  232. file.readJSON = function(filepath, options) {
  233. var src = file.read(filepath, options);
  234. var result;
  235. grunt.verbose.write('Parsing ' + filepath + '...');
  236. try {
  237. result = JSON.parse(src);
  238. grunt.verbose.ok();
  239. return result;
  240. } catch(e) {
  241. grunt.verbose.error();
  242. throw grunt.util.error('Unable to parse "' + filepath + '" file (' + e.message + ').', e);
  243. }
  244. };
  245. // Read a YAML file, parse its contents, return an object.
  246. file.readYAML = function(filepath, options) {
  247. var src = file.read(filepath, options);
  248. var result;
  249. grunt.verbose.write('Parsing ' + filepath + '...');
  250. try {
  251. result = YAML.load(src);
  252. grunt.verbose.ok();
  253. return result;
  254. } catch(e) {
  255. grunt.verbose.error();
  256. throw grunt.util.error('Unable to parse "' + filepath + '" file (' + e.problem + ').', e);
  257. }
  258. };
  259. // Write a file.
  260. file.write = function(filepath, contents, options) {
  261. if (!options) { options = {}; }
  262. var nowrite = grunt.option('no-write');
  263. grunt.verbose.write((nowrite ? 'Not actually writing ' : 'Writing ') + filepath + '...');
  264. // Create path, if necessary.
  265. file.mkdir(path.dirname(filepath));
  266. try {
  267. // If contents is already a Buffer, don't try to encode it. If no encoding
  268. // was specified, use the default.
  269. if (!Buffer.isBuffer(contents)) {
  270. contents = iconv.encode(contents, options.encoding || file.defaultEncoding);
  271. }
  272. // Actually write file.
  273. if (!nowrite) {
  274. fs.writeFileSync(filepath, contents);
  275. }
  276. grunt.verbose.ok();
  277. return true;
  278. } catch(e) {
  279. grunt.verbose.error();
  280. throw grunt.util.error('Unable to write "' + filepath + '" file (Error code: ' + e.code + ').', e);
  281. }
  282. };
  283. // Read a file, optionally processing its content, then write the output.
  284. file.copy = function(srcpath, destpath, options) {
  285. if (!options) { options = {}; }
  286. // If a process function was specified, and noProcess isn't true or doesn't
  287. // match the srcpath, process the file's source.
  288. var process = options.process && options.noProcess !== true &&
  289. !(options.noProcess && file.isMatch(options.noProcess, srcpath));
  290. // If the file will be processed, use the encoding as-specified. Otherwise,
  291. // use an encoding of null to force the file to be read/written as a Buffer.
  292. var readWriteOptions = process ? options : {encoding: null};
  293. // Actually read the file.
  294. var contents = file.read(srcpath, readWriteOptions);
  295. if (process) {
  296. grunt.verbose.write('Processing source...');
  297. try {
  298. contents = options.process(contents, srcpath);
  299. grunt.verbose.ok();
  300. } catch(e) {
  301. grunt.verbose.error();
  302. throw grunt.util.error('Error while processing "' + srcpath + '" file.', e);
  303. }
  304. }
  305. // Abort copy if the process function returns false.
  306. if (contents === false) {
  307. grunt.verbose.writeln('Write aborted.');
  308. } else {
  309. file.write(destpath, contents, readWriteOptions);
  310. }
  311. };
  312. // Delete folders and files recursively
  313. file.delete = function(filepath, options) {
  314. filepath = String(filepath);
  315. var nowrite = grunt.option('no-write');
  316. if (!options) {
  317. options = {force: grunt.option('force') || false};
  318. }
  319. grunt.verbose.write((nowrite ? 'Not actually deleting ' : 'Deleting ') + filepath + '...');
  320. if (!file.exists(filepath)) {
  321. grunt.verbose.error();
  322. grunt.log.warn('Cannot delete nonexistent file.');
  323. return false;
  324. }
  325. // Only delete cwd or outside cwd if --force enabled. Be careful, people!
  326. if (!options.force) {
  327. if (file.isPathCwd(filepath)) {
  328. grunt.verbose.error();
  329. grunt.fail.warn('Cannot delete the current working directory.');
  330. return false;
  331. } else if (!file.isPathInCwd(filepath)) {
  332. grunt.verbose.error();
  333. grunt.fail.warn('Cannot delete files outside the current working directory.');
  334. return false;
  335. }
  336. }
  337. try {
  338. // Actually delete. Or not.
  339. if (!nowrite) {
  340. rimraf.sync(filepath);
  341. }
  342. grunt.verbose.ok();
  343. return true;
  344. } catch(e) {
  345. grunt.verbose.error();
  346. throw grunt.util.error('Unable to delete "' + filepath + '" file (' + e.message + ').', e);
  347. }
  348. };
  349. // True if the file path exists.
  350. file.exists = function() {
  351. var filepath = path.join.apply(path, arguments);
  352. return fs.existsSync(filepath);
  353. };
  354. // True if the file is a symbolic link.
  355. file.isLink = function() {
  356. var filepath = path.join.apply(path, arguments);
  357. return file.exists(filepath) && fs.lstatSync(filepath).isSymbolicLink();
  358. };
  359. // True if the path is a directory.
  360. file.isDir = function() {
  361. var filepath = path.join.apply(path, arguments);
  362. return file.exists(filepath) && fs.statSync(filepath).isDirectory();
  363. };
  364. // True if the path is a file.
  365. file.isFile = function() {
  366. var filepath = path.join.apply(path, arguments);
  367. return file.exists(filepath) && fs.statSync(filepath).isFile();
  368. };
  369. // Is a given file path absolute?
  370. file.isPathAbsolute = function() {
  371. var filepath = path.join.apply(path, arguments);
  372. return path.resolve(filepath) === filepath.replace(/[\/\\]+$/, '');
  373. };
  374. // Do all the specified paths refer to the same path?
  375. file.arePathsEquivalent = function(first) {
  376. first = path.resolve(first);
  377. for (var i = 1; i < arguments.length; i++) {
  378. if (first !== path.resolve(arguments[i])) { return false; }
  379. }
  380. return true;
  381. };
  382. // Are descendant path(s) contained within ancestor path? Note: does not test
  383. // if paths actually exist.
  384. file.doesPathContain = function(ancestor) {
  385. ancestor = path.resolve(ancestor);
  386. var relative;
  387. for (var i = 1; i < arguments.length; i++) {
  388. relative = path.relative(path.resolve(arguments[i]), ancestor);
  389. if (relative === '' || /\w+/.test(relative)) { return false; }
  390. }
  391. return true;
  392. };
  393. // Test to see if a filepath is the CWD.
  394. file.isPathCwd = function() {
  395. var filepath = path.join.apply(path, arguments);
  396. try {
  397. return file.arePathsEquivalent(fs.realpathSync(process.cwd()), fs.realpathSync(filepath));
  398. } catch(e) {
  399. return false;
  400. }
  401. };
  402. // Test to see if a filepath is contained within the CWD.
  403. file.isPathInCwd = function() {
  404. var filepath = path.join.apply(path, arguments);
  405. try {
  406. return file.doesPathContain(fs.realpathSync(process.cwd()), fs.realpathSync(filepath));
  407. } catch(e) {
  408. return false;
  409. }
  410. };