UNPKG

grunt-bowercopy

Version:

Scrupulously manage file locations for bower dependencies.

338 lines (303 loc) 8.86 kB
/* * grunt-bowercopy * * Copyright (c) 2014 Timmy Willison * Licensed under the MIT license. */ module.exports = function (grunt) { 'use strict'; // Logging var log = grunt.log, verbose = grunt.verbose, fail = grunt.fail; // Utilities var _ = require('lodash'); // Modules var path = require('path'), bower = require('bower'), glob = require('glob'), sep = path.sep; // Get path to bower config file var bowerrc = grunt.file.exists('.bowerrc') ? grunt.file.readJSON('.bowerrc') : {}, bowerConfigPath = 'bower.json'; if (bowerrc.cwd) { bowerConfigPath = path.join(bowerrc.cwd, bowerConfigPath); } // Get all modules var bowerConfig = grunt.file.readJSON(bowerConfigPath); var allModules = Object.keys( _.extend({}, bowerConfig.dependencies, bowerConfig.devDependencies) ); var unused = allModules.slice(0); // Track number of runs var numTargets; var numRuns = 0; // Regex var rperiod = /\./; var rmain = /^(.+):main$/; /** * Retrieve the number of targets from the grunt config * @returns {number|undefined} Returns the number of targets, * or undefined if the bowercopy config could not be found */ function getNumTargets() { if (numTargets) { return numTargets; } var targets = grunt.config('bowercopy'); if (targets) { delete targets.options; numTargets = Object.keys(targets).length; } return numTargets; } /** * Convert from grunt to a cleaner format * @param {Array} files */ function convert(files) { var converted = []; files.forEach(function(file) { // We need originals as the destinations may not yet exist file = file.orig; var dest = file.dest; // Use destination for source if no source is available if (!file.src.length) { converted.push({ src: dest, dest: dest }); return; } file.src.forEach(function(source) { converted.push({ src: source, dest: dest }); }); }); return converted; } /** * Filter out all of the modules represented in the filesSrc array * @param {Array} modules * @param {Array} files * @param {Object} options */ function filterRepresented(modules, files, options) { return _.filter(modules, function(module) { if (options.ignore.indexOf(module) > -1) { return false; } return !_.some(files, function(file) { // Look for the module name somewhere in the source path return path.join(sep, options.srcPrefix, file.src.replace(rmain, '$1'), sep) .indexOf(sep + module + sep) > -1; }); }); } /** * Ensure all bower dependencies are accounted for * @param {Array} files Files property from the task * @param {Object} options * @returns {boolean} Returns whether all dependencies are accounted for */ function ensure(files, options) { // Update the global array of represented modules unused = filterRepresented(unused, files, options); verbose.writeln('Unrepresented modules list currently at ', unused); // Only print message when all targets have been run if (++numRuns === getNumTargets()) { if (unused.length) { if (options.clean) { log.error('Could not clean directory. Some bower components are not configured: ', unused); } else if (options.report) { log.writeln('Some bower components are not configured: ', unused); } } else { // Remove the bower_components directory as it's no longer needed if (options.clean) { grunt.file.delete(options.srcPrefix); log.ok('Bower directory cleaned'); } if (options.report) { log.ok('All modules accounted for'); } } } } /** * Convert an array of files sources to our format * @param {Array} files * @param {Object} options * @param {string} [dest] A folder destination for all of these sources */ function convertMatches(files, options, dest) { return files.map(function(source) { return { src: source, dest: path.join( // Build a destination from the new source if no dest // was specified dest != null ? dest : path.dirname(source).replace(options.srcPrefix + sep, ''), path.basename(source) ) }; }); } /** * Get the main files for a particular package * @param {string} src * @param {Object} options * @param {string} dest * @returns {Array} Returns an array of file locations from the main property */ function getMain(src, options, dest) { var meta = grunt.file.readJSON(path.join(src, '.bower.json')); if (!meta.main) { fail.fatal('No main property specified by ' + path.normalize(src.replace(options.srcPrefix, ''))); } var files = typeof meta.main === 'string' ? [meta.main] : meta.main; return files.map(function(source) { return { src: path.join(src, source), dest: dest }; }); } /** * Copy over specified component files from the bower directory * files format: [{ src: '', dest: '' }, ...] * @param {Array} files * @param {Object} options * @returns {boolean} Returns whether anything was copied for the list of files */ function copy(files, options) { var copied = false; files.forEach(function(file) { // Normalize input var src = path.normalize(file.src); // Use source for destination if no destination is available // This is done here so globbing can use the original dest var dest = path.normalize(file.dest || src); // Add source prefix if not already added if (src.indexOf(options.srcPrefix) !== 0) { src = path.join(options.srcPrefix, src); } // Add dest prefix if not already added if (dest.indexOf(options.destPrefix) !== 0) { dest = path.join(options.destPrefix, dest); } // Copy main files if :main is specified var main = rmain.exec(src); if (main) { //Trim :main from dest strings //(required if the user did not also provide an explicit dest) var temp = rmain.exec(dest); if (temp) { dest = temp[1]; } copied = copy(getMain(main[1], options, dest), options) || copied; return; } // Copy folders if (grunt.file.isDir(src)) { grunt.file.recurse(src, function(abspath, rootdir, subdir, filename) { copied = true; grunt.file.copy( abspath, path.join(dest, subdir || '', filename), options.copyOptions ); }); log.writeln(src + ' -> ' + dest); // Copy files } else if (grunt.file.exists(src)) { copied = true; if (!rperiod.test(path.basename(dest))) { dest = path.join(dest, path.basename(src)); } grunt.file.copy(src, dest, options.copyOptions); log.writeln(src + ' -> ' + dest); // Glob } else { var matches = glob.sync(file.src, { cwd: options.srcPrefix }); if (matches.length) { matches = convertMatches(matches, options, file.dest); copied = copy(matches, options) || copied; } else { fail.warn(src + ' was not found'); } } }); return copied; } /** * Top-level copying run * files format is Grunt's default: * [{ orig: { src: '', dest: '' }, src: '', dest: '' }, ...] * convert to copy()'s format before calling copy() * @param {Array} files * @param {Object} options */ var run = function(files, options) { // Normalize paths options.srcPrefix = path.normalize(options.srcPrefix); options.destPrefix = path.normalize(options.destPrefix); verbose.writeln('Using srcPrefix: ' + options.srcPrefix); verbose.writeln('Using destPrefix: ' + options.destPrefix); // Build the file list files = convert(files); // Copy files if (!copy(files, options)) { fail.warn('Nothing was copied for the "' + this.target + '" target'); } // Report if any dependencies have not been copied ensure(files, options); }; grunt.registerMultiTask( 'bowercopy', [ 'Copy only the needed files from bower components', 'over to their specified file locations' ].join(' '), function bowercopy() { var self = this; var files = this.files; var srcPrefix = ''; if (bower.config.directory && grunt.file.isPathAbsolute(bower.config.directory)) { srcPrefix = bower.config.directory; } else { srcPrefix = path.join(bower.config.cwd, bower.config.directory); } // Options var options = this.options({ srcPrefix: srcPrefix, destPrefix: '', ignore: [], report: true, runBower: true, clean: false, copyOptions: {} }); // Back-compat. Non-camelcase if (options.runBower || options.runbower) { // Run `bower install` var done = this.async(); bower.commands.install().on('log', function(result) { log.writeln(['bower', result.id.cyan, result.message].join(' ')); }).on('error', function(code) { fail.fatal(code); }).on('end', function() { run.call(self, files, options); done(); }); } else { run.call(self, files, options); } } ); };