/* * grunt * http://gruntjs.com/ * * Copyright (c) 2014 "Cowboy" Ben Alman * Licensed under the MIT license. * https://github.com/gruntjs/grunt/blob/master/LICENSE-MIT */ 'use strict'; var grunt = require('../grunt'); // Nodejs libs. var fs = require('fs'); var path = require('path'); // The module to be exported. var file = module.exports = {}; // External libs. file.glob = require('glob'); file.minimatch = require('minimatch'); file.findup = require('findup-sync'); var YAML = require('js-yaml'); var rimraf = require('rimraf'); var iconv = require('iconv-lite'); // Windows? var win32 = process.platform === 'win32'; // Normalize \\ paths to / paths. var unixifyPath = function(filepath) { if (win32) { return filepath.replace(/\\/g, '/'); } else { return filepath; } }; // Change the current base path (ie, CWD) to the specified path. file.setBase = function() { var dirpath = path.join.apply(path, arguments); process.chdir(dirpath); }; // Process specified wildcard glob patterns or filenames against a // callback, excluding and uniquing files in the result set. var processPatterns = function(patterns, fn) { // Filepaths to return. var result = []; // Iterate over flattened patterns array. grunt.util._.flatten(patterns).forEach(function(pattern) { // If the first character is ! it should be omitted var exclusion = pattern.indexOf('!') === 0; // If the pattern is an exclusion, remove the ! if (exclusion) { pattern = pattern.slice(1); } // Find all matching files for this pattern. var matches = fn(pattern); if (exclusion) { // If an exclusion, remove matching files. result = grunt.util._.difference(result, matches); } else { // Otherwise add matching files. result = grunt.util._.union(result, matches); } }); return result; }; // Match a filepath or filepaths against one or more wildcard patterns. Returns // all matching filepaths. file.match = function(options, patterns, filepaths) { if (grunt.util.kindOf(options) !== 'object') { filepaths = patterns; patterns = options; options = {}; } // Return empty set if either patterns or filepaths was omitted. if (patterns == null || filepaths == null) { return []; } // Normalize patterns and filepaths to arrays. if (!Array.isArray(patterns)) { patterns = [patterns]; } if (!Array.isArray(filepaths)) { filepaths = [filepaths]; } // Return empty set if there are no patterns or filepaths. if (patterns.length === 0 || filepaths.length === 0) { return []; } // Return all matching filepaths. return processPatterns(patterns, function(pattern) { return file.minimatch.match(filepaths, pattern, options); }); }; // Match a filepath or filepaths against one or more wildcard patterns. Returns // true if any of the patterns match. file.isMatch = function() { return file.match.apply(file, arguments).length > 0; }; // Return an array of all file paths that match the given wildcard patterns. file.expand = function() { var args = grunt.util.toArray(arguments); // If the first argument is an options object, save those options to pass // into the file.glob.sync method. var options = grunt.util.kindOf(args[0]) === 'object' ? args.shift() : {}; // Use the first argument if it's an Array, otherwise convert the arguments // object to an array and use that. var patterns = Array.isArray(args[0]) ? args[0] : args; // Return empty set if there are no patterns or filepaths. if (patterns.length === 0) { return []; } // Return all matching filepaths. var matches = processPatterns(patterns, function(pattern) { // Find all matching files for this pattern. return file.glob.sync(pattern, options); }); // Filter result set? if (options.filter) { matches = matches.filter(function(filepath) { filepath = path.join(options.cwd || '', filepath); try { if (typeof options.filter === 'function') { return options.filter(filepath); } else { // If the file is of the right type and exists, this should work. return fs.statSync(filepath)[options.filter](); } } catch(e) { // Otherwise, it's probably not the right type. return false; } }); } return matches; }; var pathSeparatorRe = /[\/\\]/g; // The "ext" option refers to either everything after the first dot (default) // or everything after the last dot. var extDotRe = { first: /(\.[^\/]*)?$/, last: /(\.[^\/\.]*)?$/, }; // Build a multi task "files" object dynamically. file.expandMapping = function(patterns, destBase, options) { options = grunt.util._.defaults({}, options, { extDot: 'first', rename: function(destBase, destPath) { return path.join(destBase || '', destPath); } }); var files = []; var fileByDest = {}; // Find all files matching pattern, using passed-in options. file.expand(options, patterns).forEach(function(src) { var destPath = src; // Flatten? if (options.flatten) { destPath = path.basename(destPath); } // Change the extension? if ('ext' in options) { destPath = destPath.replace(extDotRe[options.extDot], options.ext); } // Generate destination filename. var dest = options.rename(destBase, destPath, options); // Prepend cwd to src path if necessary. if (options.cwd) { src = path.join(options.cwd, src); } // Normalize filepaths to be unix-style. dest = dest.replace(pathSeparatorRe, '/'); src = src.replace(pathSeparatorRe, '/'); // Map correct src path to dest path. if (fileByDest[dest]) { // If dest already exists, push this src onto that dest's src array. fileByDest[dest].src.push(src); } else { // Otherwise create a new src-dest file mapping object. files.push({ src: [src], dest: dest, }); // And store a reference for later use. fileByDest[dest] = files[files.length - 1]; } }); return files; }; // Like mkdir -p. Create a directory and any intermediary directories. file.mkdir = function(dirpath, mode) { if (grunt.option('no-write')) { return; } // Set directory mode in a strict-mode-friendly way. if (mode == null) { mode = parseInt('0777', 8) & (~process.umask()); } dirpath.split(pathSeparatorRe).reduce(function(parts, part) { parts += part + '/'; var subpath = path.resolve(parts); if (!file.exists(subpath)) { try { fs.mkdirSync(subpath, mode); } catch(e) { throw grunt.util.error('Unable to create directory "' + subpath + '" (Error code: ' + e.code + ').', e); } } return parts; }, ''); }; // Recurse into a directory, executing callback for each file. file.recurse = function recurse(rootdir, callback, subdir) { var abspath = subdir ? path.join(rootdir, subdir) : rootdir; fs.readdirSync(abspath).forEach(function(filename) { var filepath = path.join(abspath, filename); if (fs.statSync(filepath).isDirectory()) { recurse(rootdir, callback, unixifyPath(path.join(subdir || '', filename || ''))); } else { callback(unixifyPath(filepath), rootdir, subdir, filename); } }); }; // The default file encoding to use. file.defaultEncoding = 'utf8'; // Whether to preserve the BOM on file.read rather than strip it. file.preserveBOM = false; // Read a file, return its contents. file.read = function(filepath, options) { if (!options) { options = {}; } var contents; grunt.verbose.write('Reading ' + 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 (options.encoding !== null) { contents = iconv.decode(contents, options.encoding || file.defaultEncoding); // Strip any BOM that might exist. if (!file.preserveBOM && contents.charCodeAt(0) === 0xFEFF) { contents = contents.substring(1); } } grunt.verbose.ok(); return contents; } catch(e) { grunt.verbose.error(); throw grunt.util.error('Unable to read "' + filepath + '" file (Error code: ' + e.code + ').', e); } }; // Read a file, parse its contents, return an object. file.readJSON = function(filepath, options) { var src = file.read(filepath, options); var result; grunt.verbose.write('Parsing ' + filepath + '...'); try { result = JSON.parse(src); grunt.verbose.ok(); return result; } catch(e) { grunt.verbose.error(); throw grunt.util.error('Unable to parse "' + filepath + '" file (' + e.message + ').', e); } }; // Read a YAML file, parse its contents, return an object. file.readYAML = function(filepath, options) { var src = file.read(filepath, options); var result; grunt.verbose.write('Parsing ' + filepath + '...'); try { result = YAML.load(src); grunt.verbose.ok(); return result; } catch(e) { grunt.verbose.error(); throw grunt.util.error('Unable to parse "' + filepath + '" file (' + e.problem + ').', e); } }; // Write a file. file.write = function(filepath, contents, options) { if (!options) { options = {}; } var nowrite = grunt.option('no-write'); grunt.verbose.write((nowrite ? 'Not actually writing ' : 'Writing ') + filepath + '...'); // Create path, if necessary. file.mkdir(path.dirname(filepath)); try { // If contents is already a Buffer, don't try to encode it. If no encoding // was specified, use the default. if (!Buffer.isBuffer(contents)) { contents = iconv.encode(contents, options.encoding || file.defaultEncoding); } // Actually write file. if (!nowrite) { fs.writeFileSync(filepath, contents); } grunt.verbose.ok(); return true; } catch(e) { grunt.verbose.error(); throw grunt.util.error('Unable to write "' + filepath + '" file (Error code: ' + e.code + ').', e); } }; // Read a file, optionally processing its content, then write the output. file.copy = function(srcpath, destpath, options) { if (!options) { options = {}; } // If a process function was specified, and noProcess isn't true or doesn't // match the srcpath, process the file's source. var process = options.process && options.noProcess !== true && !(options.noProcess && file.isMatch(options.noProcess, srcpath)); // If the file will be processed, use the encoding as-specified. Otherwise, // use an encoding of null to force the file to be read/written as a Buffer. var readWriteOptions = process ? options : {encoding: null}; // Actually read the file. var contents = file.read(srcpath, readWriteOptions); if (process) { grunt.verbose.write('Processing source...'); try { contents = options.process(contents, srcpath); grunt.verbose.ok(); } catch(e) { grunt.verbose.error(); throw grunt.util.error('Error while processing "' + srcpath + '" file.', e); } } // Abort copy if the process function returns false. if (contents === false) { grunt.verbose.writeln('Write aborted.'); } else { file.write(destpath, contents, readWriteOptions); } }; // Delete folders and files recursively file.delete = function(filepath, options) { filepath = String(filepath); var nowrite = grunt.option('no-write'); if (!options) { options = {force: grunt.option('force') || false}; } grunt.verbose.write((nowrite ? 'Not actually deleting ' : 'Deleting ') + filepath + '...'); if (!file.exists(filepath)) { grunt.verbose.error(); grunt.log.warn('Cannot delete nonexistent file.'); return false; } // Only delete cwd or outside cwd if --force enabled. Be careful, people! if (!options.force) { if (file.isPathCwd(filepath)) { grunt.verbose.error(); grunt.fail.warn('Cannot delete the current working directory.'); return false; } else if (!file.isPathInCwd(filepath)) { grunt.verbose.error(); grunt.fail.warn('Cannot delete files outside the current working directory.'); return false; } } try { // Actually delete. Or not. if (!nowrite) { rimraf.sync(filepath); } grunt.verbose.ok(); return true; } catch(e) { grunt.verbose.error(); throw grunt.util.error('Unable to delete "' + filepath + '" file (' + e.message + ').', e); } }; // True if the file path exists. file.exists = function() { var filepath = path.join.apply(path, arguments); return fs.existsSync(filepath); }; // True if the file is a symbolic link. file.isLink = function() { var filepath = path.join.apply(path, arguments); return file.exists(filepath) && fs.lstatSync(filepath).isSymbolicLink(); }; // True if the path is a directory. file.isDir = function() { var filepath = path.join.apply(path, arguments); return file.exists(filepath) && fs.statSync(filepath).isDirectory(); }; // True if the path is a file. file.isFile = function() { var filepath = path.join.apply(path, arguments); return file.exists(filepath) && fs.statSync(filepath).isFile(); }; // Is a given file path absolute? file.isPathAbsolute = function() { var filepath = path.join.apply(path, arguments); return path.resolve(filepath) === filepath.replace(/[\/\\]+$/, ''); }; // Do all the specified paths refer to the same path? file.arePathsEquivalent = function(first) { first = path.resolve(first); for (var i = 1; i < arguments.length; i++) { if (first !== path.resolve(arguments[i])) { return false; } } return true; }; // Are descendant path(s) contained within ancestor path? Note: does not test // if paths actually exist. file.doesPathContain = function(ancestor) { ancestor = path.resolve(ancestor); var relative; for (var i = 1; i < arguments.length; i++) { relative = path.relative(path.resolve(arguments[i]), ancestor); if (relative === '' || /\w+/.test(relative)) { return false; } } return true; }; // Test to see if a filepath is the CWD. file.isPathCwd = function() { var filepath = path.join.apply(path, arguments); try { return file.arePathsEquivalent(fs.realpathSync(process.cwd()), fs.realpathSync(filepath)); } catch(e) { return false; } }; // Test to see if a filepath is contained within the CWD. file.isPathInCwd = function() { var filepath = path.join.apply(path, arguments); try { return file.doesPathContain(fs.realpathSync(process.cwd()), fs.realpathSync(filepath)); } catch(e) { return false; } };