449 lines
14 KiB
JavaScript
449 lines
14 KiB
JavaScript
/*
|
|
* 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;
|
|
}
|
|
};
|