UNPKG

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