Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Merge pull request #292 from yeoman/ss/updater

Updater module
  • Loading branch information...
commit c18d4691a25f3d2b8e0a447dc5e53c716f37d8fd 2 parents a46d20b + 44a2e94
@addyosmani addyosmani authored
Showing with 307 additions and 223 deletions.
  1. +98 −95 cli/bin/yeoman
  2. +209 −128 cli/lib/plugins/updater.js
View
193 cli/bin/yeoman
@@ -1,5 +1,6 @@
#!/usr/bin/env node
var fs = require('fs'),
+ exec = require('child_process').exec,
join = require('path').join,
grunt = require('grunt'),
colors = require('colors'),
@@ -13,125 +14,127 @@ var fs = require('fs'),
async = grunt.util.async,
compiled = _.template( fs.readFileSync( join(__dirname, 'help.txt'), 'utf8' ));
+
// Returns the user's home directory in a platform agnostic way.
function getUserHome() {
return process.env[(process.platform == 'win32') ? 'USERPROFILE' : 'HOME'];
}
-// grunt with the plugin registered
-grunt.npmTasks(join(__dirname, '../'));
-
-// Get back a reference to the internal grunt cli object so that we can read
-// command line parsed options from grunt, to run our internal additional
-// logic.
+function init() {
+ // grunt with the plugin registered
+ grunt.npmTasks(join(__dirname, '../'));
-var cli = require('grunt/lib/grunt/cli');
+ // Get back a reference to the internal grunt cli object so that we can read
+ // command line parsed options from grunt, to run our internal additional
+ // logic.
-// avoid the deprecation notice: goo.gl/mk2De
-Object.defineProperty(grunt, 'utils', {
- get: function() {
- return grunt.util;
- }
-});
+ var cli = require('grunt/lib/grunt/cli');
-// command line options and remaining args
-var opts = cli.options,
- cmds = cli.tasks,
- route = cmds.join(' ').trim('');
+ // avoid the deprecation notice: goo.gl/mk2De
+ Object.defineProperty(grunt, 'utils', {
+ get: function() {
+ return grunt.util;
+ }
+ });
-// custom help, on `h5bp help`
-if(/^help/.test(route)) {
- if(/^help$/.test(route)) {
- return console.log( compiled() );
+ // command line options and remaining args
+ var opts = cli.options,
+ cmds = cli.tasks,
+ route = cmds.join(' ').trim('');
+
+ // custom help, on `h5bp help`
+ if(/^help/.test(route)) {
+ if(/^help$/.test(route)) {
+ return console.log( compiled() );
+ }
+ cli.tasks = cmds.join(':');
}
- cli.tasks = cmds.join(':');
-}
-// add the plugin version on `--version`
-if(opts.version) {
- return console.log('%s v%s', pkg.name, pkg.version);
-}
+ // add the plugin version on `--version`
+ if(opts.version) {
+ return console.log('%s v%s', pkg.name, pkg.version);
+ }
-// Matches everything after init to prevent
-// the user from seeing the default grunt init tasks
-if(/^init/.test(route)) {
- // required handling of options / arguments to workaround some internal check
- // of Grunt, and let the generators go through (init and invoked in our front
- // Grunt template)
- yeoman.generators.prepare(grunt);
-}
+ // Matches everything after init to prevent
+ // the user from seeing the default grunt init tasks
+ if(/^init/.test(route)) {
+ // required handling of options / arguments to workaround some internal check
+ // of Grunt, and let the generators go through (init and invoked in our front
+ // Grunt template)
+ yeoman.generators.prepare(grunt);
+ }
-// Inform users to run `server` instead of `watch`
-if ( /^watch/.test( route ) ) {
- return console.log('\nYeoman`s watch task is integrated within `yeoman server` to combine\n\
-the dev server, re-compilation and live reloading of changed assets.\n\n\
-Feel free to run ' + 'yeoman'.bold.red + ' ' + 'server'.bold.yellow + ' instead!');
-}
+ // Inform users to run `server` instead of `watch`
+ if ( /^watch/.test( route ) ) {
+ return console.log('\nYeoman`s watch task is integrated within `yeoman server` to combine\n\
+ the dev server, re-compilation and live reloading of changed assets.\n\n\
+ Feel free to run ' + 'yeoman'.bold.red + ' ' + 'server'.bold.yellow + ' instead!');
+ }
-// a bower command
-// Examples:
-// yeoman install jquery
-// yeoman install spine
-// yeoman install backbone (which does jquery etc too.)
-// yeoman update spine
-// yeoman lookup jquery
-// yeoman search jquery
-if(/^install|^uninstall|^search|^list|^ls|^lookup|^update/.test(route)) {
- cli.tasks = 'bower' + ':' + cmds.join(':');
-}
+ // a bower command
+ // Examples:
+ // yeoman install jquery
+ // yeoman install spine
+ // yeoman install backbone (which does jquery etc too.)
+ // yeoman update spine
+ // yeoman lookup jquery
+ // yeoman search jquery
+ if(/^install|^uninstall|^search|^list|^ls|^lookup|^update/.test(route)) {
+ cli.tasks = 'bower' + ':' + cmds.join(':');
+ }
+ /* Yeoman Insight =========================================================== */
+ async.series([function(cb) {
-/* Yeoman Upgrade =========================================================== */
-if ( /^upgrade/.test( route ) ) {
-
- // Query for the latest update (grunt is used as the pkg.name for debugging
- // purposes only)
- updater.getUpdate({ name: 'grunt', version: pkg.version }, function(update){
- console.log('Update type available is:', colors.yellow(update.severity));
- console.log('You have version', colors.blue(update.localVersion));
- console.log('Latest version is', colors.red(update.latestVersion));
- console.log('To get the latest version run:' + colors.green(' npm update yeoman -g'));
- });
-
- return console.log('Update checks complete.');
-}
+ // Are we dealing with yeoman in a test environment? If so, skip the
+ // insight prompt. This is specifically put into the environment by
+ // our test spawn helper.
+ if(process.env.yeoman_test) {
+ return cb();
+ }
-/* Yeoman Insight =========================================================== */
-async.series([function(cb) {
+ insight.init({
+ pkgname : pkg.name,
+ getUserHome: getUserHome,
+ cmds : cmds,
+ cb : cb
+ });
- // Are we dealing with yeoman in a test environment? If so, skip the
- // insight prompt. This is specifically put into the environment by
- // our test spawn helper.
- if(process.env.yeoman_test) {
- return cb();
- }
+ }, function(cb) {
- insight.init({
- pkgname : pkg.name,
- getUserHome: getUserHome,
- cmds : cmds,
- cb : cb
- });
+ // if the route is empty
+ if(/^$/.test(route)) {
+ // this is specific to an empty route code
+ console.log(pkg.name + ' v%s', pkg.version);
-}, function(cb) {
+ // we return early to prevent grunt from actually running
+ // and instead just output help.txt
+ cb();
+ return console.log( compiled() );
+ }
- // if the route is empty
- if(/^$/.test(route)) {
- // this is specific to an empty route code
- console.log(pkg.name + ' v%s', pkg.version);
+ // the grunt cli
+ grunt.cli();
- // we return early to prevent grunt from actually running
- // and instead just output help.txt
cb();
- return console.log( compiled() );
- }
- // the grunt cli
- grunt.cli();
-
- cb();
-
-}]);
+ }]);
+}
+/* Yeoman Upgrade =========================================================== */
+if ( !process.env.yeoman_test ) {
+ // TODO: Change to this before the release:
+ // updater.getUpdate({ localPackageUrl: '../../package.json' }, function() {
+ // init();
+ //});
+
+ // Query for the latest update (grunt is used as the pkg.name for debugging
+ // purposes only)
+ updater.getUpdate({ name: 'grunt', version: '0.1.13' }, function( update ) {
+ init();
+ });
+} else {
+ init();
+}
View
337 cli/lib/plugins/updater.js
@@ -1,30 +1,19 @@
-
// Updater.js: npm version checker and updater for packages
-// @author: Addy Osmani
+// @author: Addy Osmani and Sindre Sorhus
// @inspired by: npm, npm-latest
//
// Sample usage:
//
// Query for the latest update type
-// updater.getUpdate({ name: 'grunt', version: pkg.version }, function(update){
-//
-// console.log('Update type available is:', colors.yellow(update.severity));
-// console.log('You have version', colors.blue(update.localVersion));
-// console.log('Latest version is', colors.red(update.latestVersion));
-// console.log('To get the latest version run:' + colors.green(' npm update yeoman -g'));
-//
+// updater.getUpdate({ name: 'grunt', version: pkg.version }, function( error, update ) {
+// console.log('Update checking complete');
// });
//
// Alternatively, if you just want to pass in a package.json
// file directly, you can simply do:
//
-// updater.getUpdate({ localPackageUrl: '../package.json'}, function(update){
-//
-// console.log('Update type available is:', colors.yellow(update.severity));
-// console.log('You have version', colors.blue(update.localVersion));
-// console.log('Latest version is', colors.red(update.latestVersion));
-// console.log('To get the latest version run:' + colors.green(' npm update yeoman -g'));
-//
+// updater.getUpdate({ localPackageUrl: '../package.json' }, function( error, update ) {
+// console.log('Update checking complete');
// });
//
// Both will either return patch, minor, major or latest. These
@@ -44,36 +33,120 @@
// latest: you are already up to date
//
-var request = require('request'),
- colors = require('colors'),
- path = require('path'),
- fs = require('fs'),
- util = require('util'),
- EventEmitter = require('events').EventEmitter,
- childProcess = require('child_process');
+var fs = require('fs');
+var path = require('path');
+var util = require('util');
+var exec = require('child_process').exec;
+var EventEmitter = require('events').EventEmitter;
+var request = require('request');
+var colors = require('colors');
+var prompt = require('prompt');
+
+
+var updater = module.exports;
+
+
+var config = (function() {
+ var mkdirp = require('mkdirp');
+ var homeDir = process.env[ ( process.platform == 'win32' ) ? 'USERPROFILE' : 'HOME' ];
+ var folderPath = path.join( homeDir, '.config', 'npm-updater' );
+ // Function, since _packageName is not available when this is init'd
+ var filename = function() {
+ return updater._packageName + '.json';
+ };
+
+ var loadConfig = function() {
+ try {
+ return JSON.parse( fs.readFileSync( path.join( folderPath, filename() ), 'utf-8' ) || {} );
+ } catch ( err ) {
+ // Create dir if it doesn't exist
+ if ( err.errno === 34 ) {
+ mkdirp.sync( folderPath );
+ return {};
+ }
+ }
+ };
+
+ return {
+ get: function( key ) {
+ return loadConfig()[ key ];
+ },
+ set: function( key, val ) {
+ var config = loadConfig();
+ config[ key ] = val;
+ fs.writeFileSync( path.join( folderPath, filename() ), JSON.stringify( config, null, '\t' ) );
+ }
+ };
+})();
+
-updater = module.exports;
// Registry end-point
// Alternative registry mirrors
// http://85.10.209.91/%s
// http://165.225.128.50:8000/%s
-updater.registryUrl = "http://registry.npmjs.org/%s";
+updater.registryUrl = 'http://registry.npmjs.org/%s';
+
+// How often the updater should check for updates
+//updater.updateCheckInterval = 1000 * 60 * 60 * 24; // 1 day
+
+// How long it should wait until force auto-update
+//updater.updatePromptTimeLimit = 1000 * 60 * 60 * 24 * 7; // 1 week
+
+// TODO: Remove before release and uncomment aboves. Only for testing.
+updater.updateCheckInterval = 10000; // 10 sec
+updater.updatePromptTimeLimit = 20000; // 20 sec
-updater.npmParseLatest = function npmParseLatest(npmObj) {
- var versions = [];
- for (var version in npmObj.time) {
- versions.push(version);
+
+// Prompt for update
+updater.promptUpdate = function promptUpdate( cb ) {
+ prompt.start();
+ prompt.message = 'yeoman'.red;
+ prompt.get([{
+ name: 'shouldUpdate',
+ message: ( 'Do you want to upgrade ' + this._packageName + '?' ).yellow
+ }], function( err, result ) {
+ cb( !err && /^y/i.test( result.shouldUpdate ) );
+ });
+};
+
+
+// TODO(sindresorhus): Docs
+// Prefilter to be overriden if custom logic is needed
+updater.shouldUpdate = function shouldUpdate( update, cb ) {
+ var severity = update.severity;
+
+ console.log('Update available: ' + update.current.green +
+ (' (current: ' + update.latest + ')').grey );
+
+ if ( config.get('optOut') === true ) {
+ console.log('You have opted out of automatic updates');
+ console.log('Run `npm update -g yeoman` to update');
+ return cb( false );
+ }
+
+ if ( severity === 'patch' ) {
+ cb( true );
+ }
+
+ if ( severity === 'minor' ) {
+ // Force auto-update if it's past the set time limit
+ if ( new Date() - new Date( update.date ) > this.updatePromptTimeLimit ) {
+ console.log( 'Forcing update because it\'s been too long since last'.red );
+ return cb( true );
}
- var lastVersion = versions[versions.length - 1];
- var lastTime = npmObj.time[lastVersion];
+ this.promptUpdate(function( shouldUpdate ) {
+ cb( shouldUpdate );
+ });
+ }
- return {
- "version": lastVersion,
- "time": new Date(lastTime)
- };
+ if ( severity === 'major' ) {
+ this.promptUpdate(function( shouldUpdate ) {
+ cb( shouldUpdate );
+ });
+ }
};
@@ -90,87 +163,109 @@ updater.npmParseLatest = function npmParseLatest(npmObj) {
// @options.localPackageUrl: the url to a local package to be
// checked against if no package name or version are supplied
//
-// @options.fetchLatest: a boolean to indicate whether you
-// should also fetch the latest version at the same time
-//
-// cb: callback for successfully returning the
-// update type
+// cb: callback for when the update checks and update is complete
-updater.getUpdate = function getUpdate(options, cb){
+updater.getUpdate = function getUpdate( options, cb ) {
+ var localPackage, url;
+ var self = this;
+ var controller = new EventEmitter();
- var self = this, url, latest, updateType, update, controller;
- cb = cb || function(){};
-
- controller = new EventEmitter();
+ cb = cb || function() {};
// Step 1: We need a package name and version to work off.
// Ideally, supply us with the package name and version
- if(options.name === undefined || options.version === undefined){
+ if ( options.name === undefined || options.version === undefined ) {
+ // If not, we'll ascertain from a local package.json file
+ if ( options.localPackageUrl ) {
+ localPackage = require( options.localPackageUrl );
+ options.name = localPackage.name;
+ options.version = localPackage.version;
+ } else {
+ return console.error('No package name/version or local package supplied');
+ }
+ }
- // If not, we'll ascertain from a local package.json file
- if(options.localPackageUrl){
+ // Expose the packageName internally, but still
+ // make it accessible if somone would need it
+ this._packageName = options.name;
- var localPackage = JSON.parse(fs.readFileSync(options.localPackageUrl).toString());
- options.name = localPackage.name;
- options.version = localPackage.version;
+ // Create the `optOut` option, so it's easy to switch the flag
+ if ( config.get('optOut') === undefined ) {
+ config.set( 'optOut', false );
+ }
- }else{
- console.error('No package name/version or local package supplied');
- }
+ // Only check for updates on a set interval
+ if ( new Date() - config.get('lastUpdateCheck') < this.updateCheckInterval ) {
+ return;
}
- // Step 2: Query the NPM registry for the latest package
- url = util.format(this.registryUrl, options.name);
+ console.log('Starting update check...');
- request(url, function(err, response, body) {
+ // Update the last update check date
+ config.set( 'lastUpdateCheck', +new Date() );
- // Fetch issue incurred
- if (err) {
+ // Step 2: Query the NPM registry for the latest package
+ url = util.format( this.registryUrl, options.name );
- controller.emit("fetchError", {
- message: err.message,
- httpCode: response.statusCode
- });
+ request({ url: url, json: true }, function( error, response, body ) {
+ var latest, update;
- return;
+ // TODO(sindresorhus): Look into the best way to output errors, only cb or cb + emit?
- } else {
- var npmObj = JSON.parse(body);
+ // Fetch issue incurred
+ if ( error ) {
+ controller.emit('fetchError', {
+ message: error.message,
+ httpCode: response && response.statusCode
+ });
- // Whoops, package not found.
- if (npmObj.error) {
+ cb( error );
- controller.emit("npmError", {
- errType: npmObj.error, // not_found etc
- reason: npmObj.reason // additional reason
- });
+ return;
+ }
- return;
+ // Whoops, package not found
+ if ( body.error ) {
+ controller.emit('npmError', {
+ errorType: body.error, // not_found etc
+ reason: body.reason // additional reason
+ });
- } else {
+ cb( error );
- // Step 3: Package found, lets compare versions
- latest = self.npmParseLatest(npmObj);
- updateType = self.parseUpdateType(options.version, latest.version);
+ return;
+ }
- // Details to expose about the update
- update = {
- latestVersion: latest.version,
- localVersion: options.version,
- severity: updateType
- };
+ // Step 3: Package found, lets compare versions
+ latest = Object.keys( body.time ).reverse()[0];
- // Possibly deprecate: fetch latest
- if(updateType !== 'latest' && options.fetchLatest === true){
- self.npmRunUpdate(options.name);
- };
+ // Details to expose about the update
+ update = {
+ latest: latest,
+ date: body.time[ latest ],
+ current: options.version,
+ severity: self.parseUpdateType( options.version, latest )
+ };
- return cb(update);
+ if ( update.severity !== 'latest' ) {
+ self.shouldUpdate( update, function( shouldUpdate ) {
+ if ( shouldUpdate ) {
+ self.updatePackage( options.name, function( err, data ) {
+ if ( err ) {
+ console.error( '\nUpdate error', err );
+ } else {
+ console.log( '\nUpdated successfully!'.green );
}
- }
- });
+ cb( err, update );
+ });
+ } else {
+ cb( err, update );
+ }
+ });
+ }
+ });
};
@@ -178,46 +273,32 @@ updater.getUpdate = function getUpdate(options, cb){
// Compare a local package version and remote package version
// to discover what type of update (major, minor, patch) is
// available.
-updater.parseUpdateType = function parseUpdateType(currentVersion, remoteVersion){
-
- // already on latest?
- if( currentVersion === remoteVersion ){
- return 'latest';
- }else{
-
- // Regex against versions for comparison
- var current = currentVersion.split('.'),
- remote = remoteVersion.split('.');
-
- // major update?
- if( remote[2] > current[2] ){
- return 'major';
- // minor update?
- }else if( remote[1] > current[1] ){
- return 'minor';
- // patch?
- }else if( remote[0] > current[0] ){
- return 'patch';
- }else{
- return "Comparison error.";
- }
- }
-
-};
-
-
-// Run npm update against a specific package name
-updater.npmRunUpdate = function npmRunUpdate(packageName){
+updater.parseUpdateType = function parseUpdateType( current, remoteVersion ) {
+ var current, remote;
- var child = exec('npm update ' + packageName, function() {
- // complete
- });
-
- child.stdout.pipe(process.stdout);
- child.stderr.pipe(process.stderr);
+ if ( current === remoteVersion ) {
+ return 'latest';
+ }
+ current = current.split('.');
+ remote = remoteVersion.split('.');
+
+ if ( remote[0] > current[0] ) {
+ return 'major';
+ } else if ( remote[1] > current[1] ) {
+ return 'minor';
+ } else if ( remote[2] > current[2] ) {
+ return 'patch';
+ } else{
+ return 'Update comparison error';
+ }
};
-
-
+// Run `npm update` against a specific package name
+updater.updatePackage = function updatePackage( packageName, cb ) {
+ var child = exec( 'npm update ' + packageName, { cwd: __dirname }, cb );
+ console.log( 'Updating ' + packageName + '\n' );
+ child.stdout.pipe( process.stdout );
+ child.stderr.pipe( process.stderr );
+};
Please sign in to comment.
Something went wrong with that request. Please try again.