From c70e7750d80fb0db8eabff2f1da4f209567f2542 Mon Sep 17 00:00:00 2001 From: Tomas Junnonen Date: Fri, 6 Sep 2013 22:27:35 -0400 Subject: [PATCH] Version 1.0.0 --- LICENSE | 198 ++------------------------------- README.md | 64 ++++++++++- bin/npm-check-updates | 3 + lib/npm-check-updates.js | 132 ++++++++++++++++++++++ lib/versionmanager.js | 229 +++++++++++++++++++++++++++++++++++++++ package.json | 23 ++++ 6 files changed, 460 insertions(+), 189 deletions(-) create mode 100644 bin/npm-check-updates create mode 100644 lib/npm-check-updates.js create mode 100644 lib/versionmanager.js create mode 100644 package.json diff --git a/LICENSE b/LICENSE index 37ec93a1..b8a71e04 100644 --- a/LICENSE +++ b/LICENSE @@ -1,191 +1,13 @@ -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ +Copyright 2013 Tomas Junnonen -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at -1. Definitions. + http://www.apache.org/licenses/LICENSE-2.0 -"License" shall mean the terms and conditions for use, reproduction, and -distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright -owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities -that control, are controlled by, or are under common control with that entity. -For the purposes of this definition, "control" means (i) the power, direct or -indirect, to cause the direction or management of such entity, whether by -contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the -outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising -permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including -but not limited to software source code, documentation source, and configuration -files. - -"Object" form shall mean any form resulting from mechanical transformation or -translation of a Source form, including but not limited to compiled object code, -generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made -available under the License, as indicated by a copyright notice that is included -in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that -is based on (or derived from) the Work and for which the editorial revisions, -annotations, elaborations, or other modifications represent, as a whole, an -original work of authorship. For the purposes of this License, Derivative Works -shall not include works that remain separable from, or merely link (or bind by -name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version -of the Work and any modifications or additions to that Work or Derivative Works -thereof, that is intentionally submitted to Licensor for inclusion in the Work -by the copyright owner or by an individual or Legal Entity authorized to submit -on behalf of the copyright owner. For the purposes of this definition, -"submitted" means any form of electronic, verbal, or written communication sent -to the Licensor or its representatives, including but not limited to -communication on electronic mailing lists, source code control systems, and -issue tracking systems that are managed by, or on behalf of, the Licensor for -the purpose of discussing and improving the Work, but excluding communication -that is conspicuously marked or otherwise designated in writing by the copyright -owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf -of whom a Contribution has been received by Licensor and subsequently -incorporated within the Work. - -2. Grant of Copyright License. - -Subject to the terms and conditions of this License, each Contributor hereby -grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, -irrevocable copyright license to reproduce, prepare Derivative Works of, -publicly display, publicly perform, sublicense, and distribute the Work and such -Derivative Works in Source or Object form. - -3. Grant of Patent License. - -Subject to the terms and conditions of this License, each Contributor hereby -grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, -irrevocable (except as stated in this section) patent license to make, have -made, use, offer to sell, sell, import, and otherwise transfer the Work, where -such license applies only to those patent claims licensable by such Contributor -that are necessarily infringed by their Contribution(s) alone or by combination -of their Contribution(s) with the Work to which such Contribution(s) was -submitted. If You institute patent litigation against any entity (including a -cross-claim or counterclaim in a lawsuit) alleging that the Work or a -Contribution incorporated within the Work constitutes direct or contributory -patent infringement, then any patent licenses granted to You under this License -for that Work shall terminate as of the date such litigation is filed. - -4. Redistribution. - -You may reproduce and distribute copies of the Work or Derivative Works thereof -in any medium, with or without modifications, and in Source or Object form, -provided that You meet the following conditions: - -You must give any other recipients of the Work or Derivative Works a copy of -this License; and -You must cause any modified files to carry prominent notices stating that You -changed the files; and -You must retain, in the Source form of any Derivative Works that You distribute, -all copyright, patent, trademark, and attribution notices from the Source form -of the Work, excluding those notices that do not pertain to any part of the -Derivative Works; and -If the Work includes a "NOTICE" text file as part of its distribution, then any -Derivative Works that You distribute must include a readable copy of the -attribution notices contained within such NOTICE file, excluding those notices -that do not pertain to any part of the Derivative Works, in at least one of the -following places: within a NOTICE text file distributed as part of the -Derivative Works; within the Source form or documentation, if provided along -with the Derivative Works; or, within a display generated by the Derivative -Works, if and wherever such third-party notices normally appear. The contents of -the NOTICE file are for informational purposes only and do not modify the -License. You may add Your own attribution notices within Derivative Works that -You distribute, alongside or as an addendum to the NOTICE text from the Work, -provided that such additional attribution notices cannot be construed as -modifying the License. -You may add Your own copyright statement to Your modifications and may provide -additional or different license terms and conditions for use, reproduction, or -distribution of Your modifications, or for any such Derivative Works as a whole, -provided Your use, reproduction, and distribution of the Work otherwise complies -with the conditions stated in this License. - -5. Submission of Contributions. - -Unless You explicitly state otherwise, any Contribution intentionally submitted -for inclusion in the Work by You to the Licensor shall be under the terms and -conditions of this License, without any additional terms or conditions. -Notwithstanding the above, nothing herein shall supersede or modify the terms of -any separate license agreement you may have executed with Licensor regarding -such Contributions. - -6. Trademarks. - -This License does not grant permission to use the trade names, trademarks, -service marks, or product names of the Licensor, except as required for -reasonable and customary use in describing the origin of the Work and -reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. - -Unless required by applicable law or agreed to in writing, Licensor provides the -Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, -including, without limitation, any warranties or conditions of TITLE, -NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are -solely responsible for determining the appropriateness of using or -redistributing the Work and assume any risks associated with Your exercise of -permissions under this License. - -8. Limitation of Liability. - -In no event and under no legal theory, whether in tort (including negligence), -contract, or otherwise, unless required by applicable law (such as deliberate -and grossly negligent acts) or agreed to in writing, shall any Contributor be -liable to You for damages, including any direct, indirect, special, incidental, -or consequential damages of any character arising as a result of this License or -out of the use or inability to use the Work (including but not limited to -damages for loss of goodwill, work stoppage, computer failure or malfunction, or -any and all other commercial damages or losses), even if such Contributor has -been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. - -While redistributing the Work or Derivative Works thereof, You may choose to -offer, and charge a fee for, acceptance of support, warranty, indemnity, or -other liability obligations and/or rights consistent with this License. However, -in accepting such obligations, You may act only on Your own behalf and on Your -sole responsibility, not on behalf of any other Contributor, and only if You -agree to indemnify, defend, and hold each Contributor harmless for any liability -incurred by, or claims asserted against, such Contributor by reason of your -accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work - -To apply the Apache License to your work, attach the following boilerplate -notice, with the fields enclosed by brackets "[]" replaced with your own -identifying information. (Don't include the brackets!) The text should be -enclosed in the appropriate comment syntax for the file format. We also -recommend that a file or class name and description of purpose be included on -the same "printed page" as the copyright notice for easier identification within -third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/README.md b/README.md index 74162123..b5920a8c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,66 @@ npm-check-updates ================= -Find newer versions of Node.js dependencies than what your package.json allows +npm-check-updates is a tool that allows you to **instantly find any updates to all of the dependencies** in your Node.js project, regardless of the version constraints you've applied in your package.json file (unlike npm itself). + +Optionally, npm-check-updates will also upgrade your package.json file to satisfy the latest available versions, all while **maintaining your existing semantic versioning policies**. + +Put plainly, it will upgrade your "express": "3.3.x" dependency to "express": "3.4.x" when express 3.4.0 hits the scene. + +Finally you can stick to [package.json best practices](http://blog.nodejitsu.com/package-dependencies-done-right) and [semantic versioning](http://semver.org/), without having to track individual new package releases. In case you do pin the exact versions, it'll update those dependencies to the latest versions as well. + +Installation +-------------- + +``` +npm install -g npm-check-updates +``` + +Example +-------------- + +Show the new dependencies available: +``` +$ npm-check-updates + +Dependency "connect" could be updated to "2.8.x" (latest is 2.8.8) +Dependency "commander" could be updated to "2.0.x" (latest is 2.0.0) + +Run 'npm-check-updates -u' to upgrade your package.json automatically + +``` + +Upgrade a project's pacakge.json: +``` +$ npm-check-updates -u another-project/ + +Dependency "request" could be updated to "2.27.x" (latest is 2.27.0) + +package.json upgraded + +``` + +Now you just verify that your project works with the upgraded versions, commit, push, job + +How new versions are determined +-------------- + +- Direct dependencies will be increased to the latest available version: + - 2.0.1 => 2.2.0 + - 1.2 => 1.3 +- Semantic versioning policies for levels are maintained while satisfying the latest version: + - 1.2.x => 1.3.x + - 1.x => 2.x +- "Any version" is maintained: + - \* => \* +- Version constraints are maintained: + - \>0.2.x => \> 0.3.x + - \>=1.0.0 => >=1.1.0 +- Dependencies newer than the latest available version are suggested to be downgraded, as it's likely a mistake: + - 2.0.x => 1.7.x, when 1.7.10 is the latest available version + - 1.1.0 => 1.0.1, when 1.0.1 is the latest available version + +Problems? +-------------- + +Please [file an issue on github](https://github.com/tjunnone/npm-check-updates/issues). Pull requests are welcome :) diff --git a/bin/npm-check-updates b/bin/npm-check-updates new file mode 100644 index 00000000..1c49a3a0 --- /dev/null +++ b/bin/npm-check-updates @@ -0,0 +1,3 @@ +#!/usr/bin/env node + +var main = require('../lib/npm-check-updates'); diff --git a/lib/npm-check-updates.js b/lib/npm-check-updates.js new file mode 100644 index 00000000..7c62d7c9 --- /dev/null +++ b/lib/npm-check-updates.js @@ -0,0 +1,132 @@ +// npm-check-updates +// Tomas Junnonen (c) 2013 +// +// Checks a package.json file for updated NPM packages that are *not* +// satisfied by the current package.json dependency declarations. +// +// Example output: +// Dependency "express" could be updated to "3.3.x" (latest is 3.3.8) +// +// Optionally automatically upgrades the dependencies in package.json +// while maintaining your existing versioning policy. +// +// Example: +// Your package.json: "express": "3.2.x." +// Latest version upstream is 3.3.8 +// package.json after upgrade: "express": "3.3.x" +// + +var program = require('commander'); +var fs = require('fs'); +var path = require('path'); +var vm = require('./versionmanager'); + +function upgradePackageFile(packageFile, currentDependencies, upgradedDependencies, callback) { + readPackageFile(packageFile, function (error, packageData) { + if (error) { + return callback(error); + } + var newPackageData = vm.updatePackageData(packageData, currentDependencies, upgradedDependencies); + writePackageFile(packageFile, newPackageData, function (error) { + if (error) { + return callback(error); + } + callback(null); + }); + }); +} + +program + .version('1.0.0') + .usage('[options] ') + .option('-s, --silent', "Don't output anything") + .option('-u, --upgrade', 'Upgrade package.json dependencies with the latest versions while maintaining your versioning policy') + .parse(process.argv); + +vm.initialize(function () { + var packageFile = 'package.json'; + + // Check if a file or directory was specified on the command line + if (program.args[0] && fs.existsSync(program.args[0])) { + if (fs.statSync(program.args[0]).isDirectory()) + packageFile = path.join(program.args[0], packageFile); + } else if (program.args[0]) { + print(program.args[0] + " is not a valid file or directory"); + process.exit(1); + } + + if (!fs.existsSync(packageFile)) { + print("package.json not found"); + process.exit(1); + } + + vm.getCurrentDependencies(packageFile, function (error, currentDependencies) { + if (error) { + return console.error("There was an error reading the package file: " + error); + } + + var dependencyList = keysToArray(currentDependencies); + vm.getLatestVersions(dependencyList, function (error, latestVersions) { + if (error) { + return console.error("There was an error determining the latest package versions: " + error); + } + + var upgradedDependencies = vm.upgradeDependencies(currentDependencies, latestVersions); + + if (isEmpty(upgradedDependencies)) { + print("All dependencies match the latest package versions :)"); + } else { + print(''); + for (var dependency in upgradedDependencies) { + print('Dependency "' + dependency + '" could be updated to "' + upgradedDependencies[dependency] + '" (latest is ' + latestVersions[dependency] + ')'); + } + + if (!program.update) { + print("\nRun 'npm-check-updates -u' to upgrade your package.json automatically"); + } + + if (program.update) { + upgradePackageFile(packageFile, currentDependencies, upgradedDependencies, function (error) { + if (error) { + return console.error("There was an error writing the package.json file: " + error); + } + + print('\n' + packageFile + " upgraded"); + }); + } + } + }); + }); +}); + +// +// Helper functions +// + +function print(message) { + if (!program.silent) + console.log(message); +} + +function isEmpty(obj) { + return Object.keys(obj).length === 0; +} + +function readPackageFile(fileName, callback) { + fs.readFile(fileName, {encoding: 'utf8'}, callback); +} + +function writePackageFile(fileName, data, callback) { + fs.writeFile(fileName, data, callback); +} + +function keysToArray(o) { + var list = []; + + for (var key in o) { + if (o.hasOwnProperty(key)) { + list.push(key); + } + } + return list; +} diff --git a/lib/versionmanager.js b/lib/versionmanager.js new file mode 100644 index 00000000..0cc847f6 --- /dev/null +++ b/lib/versionmanager.js @@ -0,0 +1,229 @@ +var npm = require('npm'); +var readJson = require('read-package-json'); +var async = require('async'); +var semver = require('semver'); + +var npmIsInitialized = false; + +/** + * Upgrade an existing dependency declaration to satisfy the latest version + * @param declaration Current version declaration (e.g. "1.2.x") + * @param latestVersion Latest version (e.g "1.3.2") + * @returns {string} The upgraded dependency declaration (e.g. "1.3.x") + */ +function upgradeDependencyDeclaration(declaration, latestVersion) { + var newDeclaration = ""; + var versionBumped = false; + + // Maintain constraints + newDeclaration += getVersionConstraints(declaration); + declaration = declaration.substr(newDeclaration.length, declaration.length); + + var currentComponents = declaration.split('.'); + var latestComponents = latestVersion.split('.'); + var proposedComponents = []; + + for (var i in currentComponents) { + var currentDigit = currentComponents[i]; + var newDigit = latestComponents[i]; + + if (isWildDigit(currentDigit)) { // Maintain existing policy + proposedComponents.push(currentDigit); + continue; + } + + var comparison = versionDigitComparison(currentDigit, newDigit); + if (comparison < 0) { // Bump digit to match latest version + proposedComponents.push(newDigit); + versionBumped = true; + } else if (comparison > 0 && !versionBumped) { + // Unusual, but the version dependend on is larger than the currently latest version + proposedComponents.push(newDigit); + } else { + if (versionBumped) { // A bump causes subsequent non-wild version digits to revert to the latest version's + proposedComponents.push(newDigit); + } else { // Maintain existing declaration digit, as greater than or equal to new version + proposedComponents.push(currentDigit); + } + } + } + + newDeclaration += proposedComponents.join('.'); + return newDeclaration; +} + +/** + * Upgrade a dependencies collection based on latest available versions + * @param currentDependencies current dependencies collection object + * @param latestVersions latest available versions collection object + * @returns {{}} upgraded dependency collection object + */ +function upgradeDependencies(currentDependencies, latestVersions) { + var upgradedDependencies = {}; + for (var dependency in currentDependencies) { + if (currentDependencies.hasOwnProperty(dependency)) { + var latestVersion = latestVersions[dependency]; + var currentVersion = currentDependencies[dependency]; + + // Unconstrain the dependency, to allow upgrades of the form: '>1.2.x' -> '>2.0.x' + var unconstrainedCurrentVersion = currentVersion.substr(getVersionConstraints(currentVersion).length, currentVersion.length); + var isLatestVersion = semver.satisfies(latestVersion, unconstrainedCurrentVersion); + + if (!isLatestVersion) { + var upgradedDependencyString = upgradeDependencyDeclaration(currentVersion, latestVersion); + upgradedDependencies[dependency] = upgradedDependencyString; + } + } + } + + return upgradedDependencies; +} + +/** + * Compare two version digits (e.g. the x from x.y.z) + * @param d1 First component + * @param d2 Second component + * @returns {number} 1 if d1 is greater, 0 if equal (or either is a wildcard), -1 if lesser + */ +function versionDigitComparison(d1, d2) { + if (parseInt(d1, 10) > parseInt(d2, 10)) { + return 1; + } else if (d1 === d2 || isWildDigit(d1) || isWildDigit(d2)) { + return 0; + } else { + return -1; + } +} + +// Convenience function to match a "wild" version digit +function isWildDigit(d) { + return (d === 'x' || + d === '*'); +} + +function getVersionConstraints(declaration) { + var constraints = ""; + + for (var i in declaration) { + if (isNaN(declaration[i])) { + constraints += declaration[i]; + } else { + break; + } + } + + return constraints; +} + +/** + * Upgrade the dependency declarations in the package data + * @param data The package.json data, as utf8 text + * @param oldDependencies Object of old dependencies {package: version} + * @param newDependencies Object of old dependencies {package: version} + * @returns {string} The updated package data, as utf8 text + */ +function updatePackageData(data, oldDependencies, newDependencies) { + for (var dependency in newDependencies) { + var expression = '"*.' + dependency + '*.:*."' + escapeRegexp(oldDependencies[dependency] + '"'); + var regExp = new RegExp(expression, "g"); + data = data.replace(regExp, '"' + dependency + '": ' + '"' + newDependencies[dependency] + '"'); + } + + return data; +} + +/** + * Get the current dependencies from the package file + * @param packageFile path to package.json + * @param callback Called with (error, {dependencyName: version} collection) + */ +function getCurrentDependencies(packageFile, callback) { + readJson(packageFile, null, false, function (error, json) { + callback(error, json ? json.dependencies : null); + }); +} + +/** + * Query the latest version info of a package + * @param packageName The name of the package to query + * @param callback Returns a {package: version} object + */ +function getLatestPackageVersion(packageName, callback) { + if (!npmIsInitialized) { + throw new Error("initialize must be called before using the version manager"); + } + + npm.commands.view([packageName, "dist-tags.latest"], true, function (error, response) { + if (error) { + return callback(error); + } + + var versionInfo = {}; + versionInfo[packageName] = Object.keys(response)[0]; + callback(error, versionInfo); + }); +} + +/** + * Get the latest versions from the NPM repository + * @param packageList A list of package names to query + * @param callback Called with (error, {packageName: version} collection) + */ +function getLatestVersions(packageList, callback) { + async.map(packageList, getLatestPackageVersion, function (error, latestVersions) { + if (error) { + return callback(error); + } + + // Merge the array of versions into one object, for easier lookups + var latestDependencies = arrayToObject(latestVersions); + callback(error, latestDependencies); + }); +} + +/** + * Initialize the version manager + * @param callback Called when done + */ +function initialize(callback) { + npm.load({silent: true}, function () { + npmIsInitialized = true; + callback(); + }); +} + +// +// Helper functions +// + +function escapeRegexp(s) { + return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); // Thanks Stack Overflow! +} + +function arrayToObject(a) { + var o = {}; + for (var i in a) { + if (a.hasOwnProperty(i)) { + for (var key in a[i]) { + if (a[i].hasOwnProperty(key)) { + o[key] = a[i][key]; + } + } + } + } + return o; +} + +function startsWith(string, prefix) { + return(string.indexOf(prefix) === 0); +} + +// +// API +// + +exports.initialize = initialize; +exports.getCurrentDependencies = getCurrentDependencies; +exports.getLatestVersions = getLatestVersions; +exports.upgradeDependencies = upgradeDependencies; +exports.updatePackageData = updatePackageData; diff --git a/package.json b/package.json new file mode 100644 index 00000000..ed10b467 --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "npm-check-updates", + "version": "1.0.0", + "author": "Tomas Junnonen ", + "description": "Find newer versions of dependencies than what your package.json allows", + "keywords": ["npm", "check", "find", "discover", "updates", "upgrades", "dependencies", "package.json", "updater", "version", "management"], + "dependencies": { + "npm": "1.3.x", + "commander": "2.0.x", + "async": "0.2.x", + "read-package-json": "1.1.x", + "semver": "2.1.x" + }, + "main": "./lib/npm-check-updates", + "bin": { + "npm-check-updates": "./bin/npm-check-updates" + }, + "repository": { + "type": "git", + "url": "https://github.com/tjunnone/npm-check-updates.git" + }, + "homepage": "https://github.com/tjunnone/npm-check-updates" +}