diff --git a/README.md b/README.md index 04ef5f3..8c3e6c3 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ `bower-locker` is a node command line tool for providing **"pseudo"-locking** capability for a project leveraging [bower](https://bower.io/). -Bower doesn't inherently provide a locking mechanism (see https://github.com/bower/bower/issues/505). +Bower doesn't inherently provide a locking mechanism (see https://github.com/bower/bower/issues/505). Bower does allow you to specify a specific version or commit for a given dependency, and a way to specify how you would like to resolve any conflicts (i.e., within the **resolutions** block). This can be effective but it is tedious to do manually to both **"lock"** the versions, and the **"unlock"** the versions to get newer updates. @@ -37,21 +37,23 @@ This will install a global command of `bower-locker`. ### lock ```bash -bower-locker lock +bower-locker lock ``` Expects to run from within a folder that contains a `bower.json` and a `./bower_components/` folder. If there is no `bower_components` folder yet, just run `bower install` first to generate it. -It should save a copy of `bower.json` as `bower-locker.bower.json` and then change `bower.json` to be a "locked" version with an additional "bowerLocker" section. +It should save a copy of `bower.json` as `bower-locker.bower.json` and then change `bower.json` to be a "locked" version with an additional "bowerLocker" section. The "bowerLocker" property object that contains the "lastUpdated" timestamp for when the locked version was generated. It also contains a "versions" property object within "bowerLocker" which records the versions that were locked in as a version number to more easily know what version we are using for each dependency. Using the `-v` flag will output the bower dependency versions that are being locked. +Using the `-s` flag will preserve the "dependencies"/"devDependencies" distinction and omit extraneous (unsaved) packages in `bower_components`. + ### unlock ```bash -bower-locker unlock +bower-locker unlock ``` Expects to run from within a folder that contains a `bower.json` and a `bower-locker.bower.json`.file. @@ -61,7 +63,7 @@ Use this command to unlock the bower file for manual updates and edits. When do ### validate ```bash -bower-locker validate +bower-locker validate ``` Expects to run from within a folder that contains a `bower.json` and a `./bower_components/` folder. @@ -73,7 +75,7 @@ Run validate to make sure that all `bower_components` were installed as expected ### status ```bash -bower-locker status +bower-locker status ``` Expects to run from within a folder that contains a `bower.json`. diff --git a/bower-locker-common.js b/bower-locker-common.js index ab68ed8..68ea0c6 100644 --- a/bower-locker-common.js +++ b/bower-locker-common.js @@ -16,7 +16,9 @@ function mapDependencyData(bowerInfo) { commit: bowerInfo._resolution !== undefined ? bowerInfo._resolution.commit : undefined, release: bowerInfo._release, src: bowerInfo._source, - originalSrc: bowerInfo._originalSource + originalSrc: bowerInfo._originalSource, + dependencies: Object.keys(bowerInfo.dependencies || {}), + devDependencies: Object.keys(bowerInfo.devDependencies || {}) }; } @@ -58,7 +60,7 @@ function getBowerFolder() { /** * Function to return the metadata for all the dependencies loaded within the `bower_components` directory - * @returns {Object} Returns dependency object for each dependency containing (dirName, commit, release, src, etc.) + * @returns {Array} Returns dependency object for each dependency containing (dirName, commit, release, src, etc.) */ function getAllDependencies() { var folder = './' + getBowerFolder(); @@ -77,8 +79,60 @@ function getAllDependencies() { return dependencyInfos; } +/** + * Recursively collects dependency info for root dependencies and their dependencies. Collected dependencies are + * removed from dependenciesMap to avoid duplication + * @param {Object} dependenciesMap Map of all uncollected dependencies + * @param {Array} roots Dependency names + * @returns {Array} dependency infos + */ +function collectDependencies(dependenciesMap, roots) { + return roots.reduce(function(dependencies, dependencyName) { + if (dependenciesMap[dependencyName]) { + var dependencyInfo = dependenciesMap[dependencyName]; + dependenciesMap[dependencyName] = null; + dependencies.push(dependencyInfo); + dependencies = dependencies.concat(collectDependencies(dependenciesMap, dependencyInfo.dependencies)); + } + + return dependencies; + }, []); +} + +/** + * Function to return the metadata for dependencies loaded within the `bower_components` directory, bucketted by + * the type of saved reference to the dependencies in bower.json, i.e., dependencies, devDependencies, or unsaved + * @returns {Object} Returns object with `dependencies`, `devDependencies`, and `unsaved` arrays containing dependency + * objects for each project. Projects that are directly or indirectly required by both dependencies and + * devDependencies are returned only in `dependencies` to avoid duplication + */ +function getDependenciesByRef() { + var dependenciesMap = getAllDependencies().reduce(function(dependenciesMap, dep) { + dependenciesMap[dep.dirName] = dep; + return dependenciesMap; + }, {}); + + var projectInfo = getDependency('./bower.json'); + var dependencies = collectDependencies(dependenciesMap, projectInfo.dependencies); + var devDependencies = collectDependencies(dependenciesMap, projectInfo.devDependencies); + var unsaved = Object.keys(dependenciesMap).reduce(function(unsaved, key) { + if (dependenciesMap[key]) { + unsaved.push(dependenciesMap[key]); + } + + return unsaved; + }, []); + + return { + dependencies: dependencies, + devDependencies: devDependencies, + unsaved: unsaved + }; +} + module.exports = { getAllDependencies: getAllDependencies, + getDependenciesByRef: getDependenciesByRef, getDependency: getDependency, mapDependencyData: mapDependencyData }; diff --git a/bower-locker-lock.js b/bower-locker-lock.js index aea0255..74d8464 100644 --- a/bower-locker-lock.js +++ b/bower-locker-lock.js @@ -10,17 +10,30 @@ var formatConfig = { size: 2 }; +/** + * Comparison function for sorting dependencies by their directory name + * @param {Object} dep1 First dependency info object to compare + * @param {Object} dep2 Second dependency info object to compare + * @returns {Number} -1 if dep1 should precede dep2, 0 if they have equivalent directory names, 1 if dep1 should + * follow dep2 + */ +function compareByDirName(dep1, dep2) { + return dep1.dirName < dep2.dirName ? -1 : dep1.dirName > dep2.dirName ? 1 : 0; +} + /** * Function to lock `bower.json` with the components that currently exist within the `bower_components` directory * This is accomplished by: * * Saving a copy of `bower.json` as `bower-locker.unlocked.json` * * Getting a list of ALL flattened dependencies, current versions and commit ids within the `bower_components` * * Load the `bower.json` into memory as a JS object for manipulation - * * Override the `dependencies` and `resolutions` blocks with values specific to the current versions + * * Override the `dependencies`, `resolutions`, and optionally `devDependencies` blocks with values specific to the + * current versions * * Save the updated (i.e., locked) `bower.json` * @param {Boolean} isVerbose Flag to indicate whether we should log verbosely or not + * @param {Boolean} saved Flag to indicate whether to only lock saved dependencies and separate devDependencies */ -function lock(isVerbose) { +function lock(isVerbose, saved) { if (isVerbose) { console.log('Start locking ...'); } @@ -34,27 +47,59 @@ function lock(isVerbose) { process.exit(1); } - // Load all dependencies from the bower_components folder - var dependencies = bowerInfo.getAllDependencies(); - // Create new bower config from existing bowerConfig.bowerLocker = {lastUpdated: (new Date()).toISOString(), lockedVersions: {}}; bowerConfig.resolutions = {}; bowerConfig.dependencies = {}; - // Remove devDependency section to prevent version collision - delete bowerConfig.devDependencies; - dependencies.forEach(function(dep) { + function addDependency(dependencyMapName, dep) { // NOTE: Use dirName as the dependency name as it is more accurate than .bower.json properties var name = dep.dirName; var version = dep.commit !== undefined ? dep.commit : dep.release; - bowerConfig.dependencies[name] = dep.src + '#' + version; // _source + bowerConfig[dependencyMapName][name] = dep.src + '#' + version; // _source bowerConfig.resolutions[name] = version; bowerConfig.bowerLocker.lockedVersions[name] = dep.release; if (isVerbose) { console.log(' %s (%s): %s', name, dep.release, dep.commit); } - }); + } + + if (saved) { + bowerConfig.devDependencies = {}; + var allDependencies = bowerInfo.getDependenciesByRef(); + + // Sort for consistent ordering/cleaner diffs + allDependencies.dependencies.sort(compareByDirName); + allDependencies.devDependencies.sort(compareByDirName); + + if (isVerbose) { + console.log('Dependencies:'); + } + + allDependencies.dependencies.forEach(addDependency.bind(null, 'dependencies')); + + if (isVerbose) { + console.log('\nDev Dependencies:'); + } + + allDependencies.devDependencies.forEach(addDependency.bind(null, 'devDependencies')); + + if (allDependencies.unsaved.length) { + if (isVerbose) { + var unsavedNames = allDependencies.unsaved.map((dep) => ' ' + dep.dirName + '\n').join(''); + console.warn('\nThe following unsaved dependencies have not been locked:\n' + unsavedNames); + } else { + console.warn('Found unsaved dependencies in bower_components. These are not locked.\n' + + 'Run with --verbose or use \'bower-locker validate\' for more details.'); + } + } + } else { + // Remove devDependency section to prevent version collision + delete bowerConfig.devDependencies; + var allDependencies = bowerInfo.getAllDependencies(); + allDependencies.forEach(addDependency.bind(null, 'dependencies')); + } + // Create copy of original bower.json fs.writeFileSync('bower-locker.bower.json', bowerConfigStr, {encoding: 'utf8'}); // Replace bower.json with 'locked' version diff --git a/bower-locker.js b/bower-locker.js index fb8224c..39ca80b 100644 --- a/bower-locker.js +++ b/bower-locker.js @@ -21,9 +21,10 @@ program .command('validate', 'validate that the currently locked bower.json matches the bower_components') .command('status', 'show the current status of the bower.json whether locked or not') .option('-v, --verbose', 'turn on verbose output') + .option('-s, --saved', 'lock only saved dependencies and devDependencies, warn on unsaved projects') .action(function(cmd) { if (cmd in bowerLocker) { - return bowerLocker[cmd](program.verbose); + return bowerLocker[cmd](program.verbose, program.saved); } else { console.error("Unknown bower-lock command. Run 'bower-lock -h' to see options."); process.exit(1);