UNPKG

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