diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..b7ca95b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +# Auto detect text files and perform LF normalization +* text=auto + +# JS files must always use LF for tools to work +*.js eol=lf diff --git a/.gitignore b/.gitignore index 679bd82..6716162 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,6 @@ config.json last-action plugins.db retry.db +error.log node_modules dist/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..479c2b8 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,5 @@ +Welcome! Thanks for your interest in contributing to plugins.jquery.com. You're **almost** in the right place. More information on how to contribute to this and all other jQuery Foundation projects is over at [contribute.jquery.org](http://contribute.jquery.org). You'll definitely want to take a look at the articles on contributing [to our websites](http://contribute.jquery.org/web-sites/) and [code](http://contribute.jquery.org/code). + +You may also want to take a look at our [commit & pull request guide](http://contribute.jquery.org/commits-and-pull-requests/) and [style guides](http://contribute.jquery.org/style-guide/) for instructions on how to maintain your fork and submit your code. Before we can merge any pull request, we'll also need you to sign our [contributor license agreement](http://contribute.jquery.org/cla). + +You can find us on [IRC](http://irc.jquery.org), specifically in #jquery-content should you have any questions. If you've never contributed to open source before, we've put together [a short guide with tips, tricks, and ideas on getting started](http://contribute.jquery.org/open-source/). diff --git a/LICENSE.txt b/LICENSE.txt index 5a0c597..8ec0732 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,13 @@ -Copyright (c) 2011 jQuery Team, http://jquery.org/team +Copyright jQuery Foundation and other contributors, https://jquery.org/ + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/jquery/plugins.jquery.com + +The following license applies to all parts of this software except as +documented below: + +==== Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the @@ -18,3 +27,10 @@ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +==== + +All files located in the node_modules directory are externally +maintained libraries used by this software which have their own +licenses; we recommend you read them, as their terms may differ from the +terms above. diff --git a/README.md b/README.md index 6d9071d..f898df8 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,29 @@ The jQuery Plugins site, http://plugins.jquery.com/ ### How it works -The plugins site is an index of GitHub repositories that contain jQuery plugins. The repositories can contain one or many jQuery plugin with an accompanying valid `plugin.jquery.json` manifest file in the repository root. The specification for this file lives [here](plugins.jquery.com/docs/package-manifest). +The plugins site is an index of GitHub repositories that contain jQuery plugins. +The repositories can contain one or many jQuery plugin with an accompanying +valid `plugin.jquery.json` manifest file in the repository root. The +specification for this file lives [here](http://plugins.jquery.com/docs/package-manifest). ### How to list a plugin -Simply add a [post-receive hook](http://help.github.com/post-receive-hooks/) to your repository with our web hook url, `http://plugins.jquery.com/postreceive-hook.`. +Simply add a [post-receive hook](http://help.github.com/post-receive-hooks/) to +your repository with our Web Hook URL, `http://plugins.jquery.com/postreceive-hook.`. +When you push to your repository, the plugins site will look at your repository's +tags and their corresponding manifest file (thepluginname.jquery.json). You can +read up on this process, as well as the requirements of the manifest file on +[the jQuery Plugins Site](http://plugins.jquery.com/docs/publish/). + +Assuming there were no errors in your manifest file, your plugin should be on +the plugins site within a minute after pushing to GitHub. If you still don't see +your plugin listed, check the [error log](http://plugins.jquery.com/error.log). + +We are currently exploring options to provide better feedback on errors encountered +during the process of adding your plugin to the plugins site. If you are still +encountering issues after verifying the post-receive hook is in place and that +your manifest file is valid, ask for assistance in #jquery-content +on [freenode.net](http://freenode.net). ## Development @@ -32,21 +50,51 @@ Simply add a [post-receive hook](http://help.github.com/post-receive-hooks/) to 1. Follow https://github.com/joyent/node/wiki/Installation +You can also install [nave](https://github.com/isaacs/nave), a node version manager. +You can easily install it using [nave-installer](https://github.com/danheberden/nave-installer) +or download it manually. + #### plugins.jquery.com setup -1. `git clone git@github.com:jquery/plugins.jquery.com.git` +To build and deploy your changes for previewing in a +[`jquery-wp-content`](https://github.com/jquery/jquery-wp-content) instance, +follow the [workflow instructions](http://contribute.jquery.org/web-sites/#workflow) +from our documentation on +[contributing to jQuery Foundation web sites](http://contribute.jquery.org/web-sites/). + +If you want to setup and ultimately run the node scripts that manage plugin +entries, run `grunt setup`. If you need to clear the db or are getting and error +running `grunt setup` regarding the setupdb or retrydb tasks failing, +run `grunt clean-all`. + +If you have made changes to the documentation and simply want to deploy or update +that content, run `grunt update`. + +#### Running the site for development and debugging -2. `cd plugins.jquery.com` +1. `node scripts/update-server.js --console` will start the update server and +log its output to the terminal window. This will *not* update wordpress, but +will let you see the result of adding a plugin locally. -3. `npm install` +2. `node scripts/wordpress-update.js --console` will process the changes in +sqlite into entries in wordpress. Note, if you're re-adding plugins that have +already been added, you will need to remove those entries from wordpress. -4. `cp config-sample.json config.json` +### Running the site normally -5. Edit config.json - * Set `wordpress` properties to contain a valid username and password for the WordPress site. +`node scripts/manager.js` runs the update-server and wordpress-update scripts +automatically. However, because it handless restarts/failures of these scripts, +it is harder to stop this process. Also, running the servers manually and +individually is much easier for development, as you will probably only *need* +update-server.js running. -6. `grunt setup` +### Transferring ownership of a plugin -### Running the site +On occassion, a plugin will be transferred from one owner to another. When this +happens, you will need to verify that the transfer is legitimate. The request +should come from the original owner, but in rare circumstances the request may +come from the new owner and the original owner may not be reachable. -`node scripts/manager.js` +To transfer a plugin, log into the production server and run the `bin/transfer.js` +script. The script will prompt you for the necessary information and has several +checks to ensure that the data provided isn't junk. diff --git a/bin/transfer.js b/bin/transfer.js new file mode 100755 index 0000000..f956610 --- /dev/null +++ b/bin/transfer.js @@ -0,0 +1,106 @@ +#!/usr/bin/env node + +var Step = require( "step" ), + service = require( "../lib/service" ), + pluginsDb = require( "../lib/pluginsdb" ); + +process.stdin.setEncoding( "utf8" ); + +function prompt( message, fn ) { + process.stdout.write( message + " " ); + process.stdin.resume(); + + process.stdin.once( "data", function( chunk ) { + process.stdin.pause(); + fn( null, chunk.trim() ); + }); +} + +function showError( error ) { + console.log( "Error transferring ownership" ); + console.log( error.stack ); + process.exit( 1 ); +} + +function transfer( fn ) { + var plugin; + + Step( + function() { + // Find out which plugin to transfer + prompt( "Plugin:", this ); + }, + + function( error, _plugin ) { + if ( error ) { + return showError( error ); + } + + plugin = _plugin; + + // Find out who currently owns the plugin + pluginsDb.getOwner( plugin, this.parallel() ); + }, + + function( error, actualOwner ) { + if ( error ) { + return showError( error ); + } + + // Verify the plugin exists + if ( !actualOwner ) { + console.log( plugin + " does not exist." ); + process.exit( 1 ); + } + + // Find out who we think owns the plugin + this.parallel()( null, actualOwner ); + prompt( "Current owner:", this.parallel() ); + }, + + function( error, actualOwner, providedOwner ) { + if ( error ) { + return showError( error ); + } + + // Verify the expected owner is the real owner + if ( providedOwner !== actualOwner ) { + console.log( plugin + " is owned by " + actualOwner + + ", not " + providedOwner + "." ); + process.exit( 1 ); + } + + // Find out where the plugin is being transferred to + prompt( "New repository id (e.g., github/owner/repo)", this ); + }, + + function( error, id ) { + if ( error ) { + return showError( error ); + } + + // Create a Repo instance to verify the new id and parse the data + var repo; + try { + repo = service.getRepoById( id ); + } catch ( error ) { + fn( error ); + return; + } + + // Transfer ownersip + this.parallel()( null, repo.userId ); + pluginsDb.transferOwnership( plugin, repo.userId, repo.id, this.parallel() ); + }, + + function( error, owner ) { + if ( error ) { + return showError( error ); + } + + console.log( "Succesfully transferred " + plugin + " to " + owner + "." ); + } + ); +} + +transfer(); diff --git a/config-sample.json b/config-sample.json index 3bb0433..90bce25 100644 --- a/config-sample.json +++ b/config-sample.json @@ -2,7 +2,7 @@ "repoDir": "/tmp/plugin-repos", "pluginsDb": "plugins.db", "wordpress": { - "url": "dev.plugins.jquery.com", + "url": "vagrant.plugins.jquery.com", "username": "admin", "password": "secret" } diff --git a/grunt.js b/grunt.js index ffd1851..9dbff99 100644 --- a/grunt.js +++ b/grunt.js @@ -1,16 +1,16 @@ -var config = require( "./lib/config" ); +var path = require( "path" ), + rimraf = require( "rimraf" ), + config = require( "./lib/config" ); module.exports = function( grunt ) { +var async = grunt.utils.async; + grunt.loadNpmTasks( "grunt-wordpress" ); -grunt.loadNpmTasks( "grunt-clean" ); grunt.loadNpmTasks( "grunt-jquery-content" ); grunt.loadNpmTasks( "grunt-check-modules" ); grunt.initConfig({ - clean: { - wordpress: "dist/" - }, lint: { grunt: "grunt.js", src: [ "lib/**", "scripts/**" ] @@ -19,23 +19,24 @@ grunt.initConfig({ grunt: { options: grunt.file.readJSON( ".jshintrc" ) }, src: { options: grunt.file.readJSON( ".jshintrc" ) } }, - watch: { - docs: { - files: "pages/**", - tasks: "docs" - } - }, test: { files: [ "test/**/*.js" ] }, "build-pages": { all: grunt.file.expandFiles( "pages/**" ) }, + "build-resources": { + all: grunt.file.expandFiles( "resources/**" ) + }, wordpress: grunt.utils._.extend({ dir: "dist/wordpress" }, config.wordpress ) }); +grunt.registerTask( "clean", function() { + rimraf.sync( "dist" ); +}); + // We only want to sync the documentation, so we override wordpress-get-postpaths // to only find pages. This ensures that we don't delete all of the plugin posts. grunt.registerHelper( "wordpress-get-postpaths", function( fn ) { @@ -53,9 +54,49 @@ grunt.registerHelper( "wordpress-get-postpaths", function( fn ) { }); }); +grunt.registerMultiTask( "build-resources", "Copy resources", function() { + var task = this, + taskDone = task.async(), + files = this.data, + targetDir = grunt.config( "wordpress.dir" ) + "/resources/"; + + grunt.file.mkdir( targetDir ); + + grunt.utils.async.forEachSeries( files, function( fileName, fileDone ) { + grunt.file.copy( fileName, targetDir + fileName.replace( /^.+?\//, "" ) ); + fileDone(); + }, function() { + if ( task.errorCount ) { + grunt.warn( "Error building resources." ); + return taskDone( false ); + } + + grunt.log.writeln( "Built " + files.length + " resources." ); + + // Build validate.js + grunt.file.write( targetDir + "/validate.js", + "(function() {" + + grunt.file.read( require.resolve( "semver" ) ) + ";" + + grunt.file.read( "lib/manifest.js" ) + + grunt.file.read( "resources/validate.js" ) + + "})();" ); + + taskDone(); + }); +}); + grunt.registerTask( "sync-docs", function() { - var done = this.async(); - grunt.helper( "wordpress-sync-posts", "dist/wordpress/posts/", function( error ) { + var done = this.async(), + dir = grunt.config( "wordpress.dir" ); + + async.waterfall([ + function syncPosts( fn ) { + grunt.helper( "wordpress-sync-posts", path.join( dir, "posts/" ), fn ); + }, + function syncResources( fn ) { + grunt.helper( "wordpress-sync-resources", path.join( dir, "resources/" ), fn ); + } + ], function( error ) { if ( error ) { return done( false ); } @@ -67,8 +108,7 @@ grunt.registerTask( "sync-docs", function() { // clean-all will delete EVERYTHING, including the plugin registery. This is // useful only for development if you want a clean slate to test from. grunt.registerTask( "clean-all", function() { - var rimraf = require( "rimraf" ), - retry = require( "./lib/retrydb" ); + var retry = require( "./lib/retrydb" ); // clean repo checkouts rimraf.sync( config.repoDir ); @@ -132,9 +172,12 @@ grunt.registerTask( "restore-repos", function() { }); }); + + grunt.registerTask( "default", "lint test" ); -grunt.registerTask( "setup", "setup-pluginsdb setup-retrydb sync-docs" ); -grunt.registerTask( "update", "clean build-pages sync-docs" ); -grunt.registerTask( "restore", "clean-retries setup-retrydb sync-docs restore-repos" ); +grunt.registerTask( "publish-docs", "build-pages build-resources sync-docs" ); +grunt.registerTask( "setup", "setup-pluginsdb setup-retrydb publish-docs" ); +grunt.registerTask( "update", "clean publish-docs" ); +grunt.registerTask( "restore", "clean-retries setup-retrydb publish-docs restore-repos" ); }; diff --git a/lib/config.js b/lib/config.js index 7dd033c..72c2f59 100644 --- a/lib/config.js +++ b/lib/config.js @@ -12,5 +12,6 @@ function resolvePath( key, _default ) { resolvePath( "repoDir", path.resolve( tmpDir, "plugin-repos" ) ); resolvePath( "pluginsDb", "plugins.db" ); resolvePath( "lastActionFile", "last-action" ); +resolvePath( "errorLog", "error.log" ); module.exports = config; diff --git a/lib/hook.js b/lib/hook.js index 2c14e26..954fd05 100644 --- a/lib/hook.js +++ b/lib/hook.js @@ -129,9 +129,22 @@ function processRelease( repo, tag, file, manifest, fn ) { // the plugin is owned by someone else if ( owner !== repo.userId ) { - // TODO: report error to user logger.log( repo.userId + " attempted to add " + manifest.name + " which is owned by " + owner ); - return fn( null, null ); + repo.informOtherOwner({ + tag: tag, + name: manifest.name, + owner: owner + }); + + // track the tag so we don't process it on the next update + pluginsDb.addTag( repo.id, tag, function( error ) { + if ( error ) { + return fn( error ); + } + + fn( null, null ); + }); + return; } return owner; @@ -149,6 +162,7 @@ function processRelease( repo, tag, file, manifest, fn ) { return fn( error ); } + repo.informSuccess({ name: manifest.name, version: manifest.version }); logger.log( "Added " + manifest.name + " v" + manifest.version + " to plugins DB" ); fn( null, manifest ); } diff --git a/lib/manifest.js b/lib/manifest.js new file mode 100644 index 0000000..921a63d --- /dev/null +++ b/lib/manifest.js @@ -0,0 +1,243 @@ +(function ( exports, semver ) { + +exports.blacklist = []; +exports.suites = []; + +function isObject( obj ) { + return ({}).toString.call( obj ) === "[object Object]"; +} + +function isUrl( /*str*/ ) { + // TODO: URL validation + return true; +} + +function isEmail( str ) { + return (/^[a-zA-Z0-9.!#$%&'*+\/=?\^_`{|}~\-]+@[a-zA-Z0-9\-]+(?:\.[a-zA-Z0-9\-]+)*$/).test( str ); +} + +exports.validate = function( manifest, version, prefix, filename ) { + var errors = []; + + /** required fields **/ + + if ( !manifest.name ) { + errors.push( "Missing required field: name." ); + } else if ( typeof manifest.name !== "string" ) { + errors.push( "Invalid data type for name; must be a string." ); + } else if ( !(/^[a-zA-Z0-9_\.\-]+$/).test( manifest.name ) ) { + errors.push( "Name contains invalid characters." ); + } else if ( exports.blacklist.indexOf( manifest.name ) !== -1 ) { + errors.push( "Name must not be '" + manifest.name + "'." ); + } else { + if ( prefix ) { + if ( manifest.name.indexOf( prefix ) !== 0 ) { + errors.push( "Name must start with '" + prefix + "'." ); + } + } else { + Object.keys( exports.suites ).forEach(function( repoId ) { + var prefix = exports.suites[ repoId ]; + if ( manifest.name.indexOf( prefix ) === 0 && + !(/\./).test( manifest.name.substr( prefix.length ) ) ) { + errors.push( "Name must not start with '" + prefix + "'." ); + } + }); + } + + if ( filename && filename.substr( 0, filename.length - 12 ) !== manifest.name ) { + errors.push( "Name must match manifest file name." ); + } + } + + if ( !manifest.version ) { + errors.push( "Missing required field: version." ); + } else if ( typeof manifest.version !== "string" ) { + errors.push( "Invalid data type for version; must be a string." ); + } else if ( manifest.version !== semver.clean( manifest.version ) ) { + errors.push( "Manifest version (" + manifest.version + ") is invalid." ); + // version may not be provided when run as a standalone validator + } else if ( version && (manifest.version !== semver.clean( version )) ) { + errors.push( "Manifest version (" + manifest.version + ") does not match tag (" + version + ")." ); + } + + if ( !manifest.title ) { + errors.push( "Missing required field: title." ); + } else if ( typeof manifest.title !== "string" ) { + errors.push( "Invalid data type for title; must be a string." ); + } + + if ( !manifest.author ) { + errors.push( "Missing required field: author." ); + } else if ( !isObject( manifest.author ) ) { + errors.push( "Invalid data type for author; must be an object." ); + } else if ( !manifest.author.name ) { + errors.push( "Missing required field: author.name." ); + } else { + if ( typeof manifest.author.name !== "string" ) { + errors.push( "Invalid data type for author.name; must be a string." ); + } + + if ( "email" in manifest.author ) { + if ( typeof manifest.author.email !== "string" ) { + errors.push( "Invalid data type for author.email; must be a string." ); + } else if ( !isEmail( manifest.author.email ) ) { + errors.push( "Invalid value for author.email." ); + } + } + + if ( "url" in manifest.author ) { + if ( typeof manifest.author.url !== "string" ) { + errors.push( "Invalid data type for author.url; must be a string." ); + } else if ( !isUrl( manifest.author.url ) ) { + errors.push( "Invalid value for author.url." ); + } + } + } + + if ( !manifest.licenses ) { + errors.push( "Missing required field: licenses." ); + } else if ( !Array.isArray( manifest.licenses ) ) { + errors.push( "Invalid data type for licenses; must be an array." ); + } else if ( !manifest.licenses.length ) { + errors.push( "There must be at least one license." ); + } else { + manifest.licenses.forEach(function( license, i ) { + if ( !license.url ) { + errors.push( "Missing required field: licenses[" + i + "].url." ); + } else if ( typeof license.url !== "string" ) { + errors.push( "Invalid data type for licenses[" + i + "].url; must be a string." ); + } else if ( !isUrl( license.url ) ) { + errors.push( "Invalid value for license.url." ); + } + }); + } + + if ( !manifest.dependencies ) { + errors.push( "Missing required field: dependencies." ); + } else if ( !isObject( manifest.dependencies ) ) { + errors.push( "Invalid data type for dependencies; must be an object." ); + } else { + if ( !manifest.dependencies.jquery ) { + errors.push( "Missing required dependency: jquery." ); + } + Object.keys( manifest.dependencies ).forEach(function( dependency ) { + if ( typeof manifest.dependencies[ dependency ] !== "string" ) { + errors.push( "Invalid data type for dependencies[" + dependency + "];" + + " must be a string." ); + } else if ( semver.validRange( manifest.dependencies[ dependency ] ) === null ) { + errors.push( "Invalid version range for dependency: " + dependency + "." ); + } + }); + } + + /** optional fields **/ + + if ( "description" in manifest && typeof manifest.description !== "string" ) { + errors.push( "Invalid data type for description; must be a string." ); + } + + if ( "keywords" in manifest ) { + if ( !Array.isArray( manifest.keywords ) ) { + errors.push( "Invalid data type for keywords; must be an array." ); + } else { + manifest.keywords.forEach(function( keyword, i ) { + if ( typeof keyword !== "string" ) { + errors.push( "Invalid data type for keywords[" + i + "]; must be a string." ); + } else if ( !(/^[a-zA-Z0-9\.\-]+$/).test( keyword ) ) { + errors.push( "Invalid characters for keyword: " + keyword + "." ); + } + }); + } + } + + if ( "homepage" in manifest ) { + if ( typeof manifest.homepage !== "string" ) { + errors.push( "Invalid data type for homepage; must be a string." ); + } else if ( !isUrl( manifest.homepage ) ) { + errors.push( "Invalid value for homepage." ); + } + } + + if ( "docs" in manifest ) { + if ( typeof manifest.docs !== "string" ) { + errors.push( "Invalid data type for docs; must be a string." ); + } else if ( !isUrl( manifest.docs ) ) { + errors.push( "Invalid value for docs." ); + } + } + + if ( "demo" in manifest ) { + if ( typeof manifest.demo !== "string" ) { + errors.push( "Invalid data type for demo; must be a string." ); + } else if ( !isUrl( manifest.demo ) ) { + errors.push( "Invalid value for demo." ); + } + } + + if ( "download" in manifest ) { + if ( typeof manifest.download !== "string" ) { + errors.push( "Invalid data type for download; must be a string." ); + } else if ( !isUrl( manifest.download ) ) { + errors.push( "Invalid value for download." ); + } + } + + if ( "bugs" in manifest ) { + // check { url: "..." } format + if ( typeof manifest.bugs === "object" ) { + if ( typeof manifest.bugs.url !== "string" ) { + errors.push( "Invalid data type for bugs.url; must be a string." ); + } else if ( !isUrl( manifest.bugs.url ) ) { + errors.push( "Invalid value for bugs.url." ); + } + } else { + if ( typeof manifest.bugs !== "string" ) { + errors.push( "Invalid data type for bugs; must be a string." ); + } else if ( !isUrl( manifest.bugs ) ) { + errors.push( "Invalid value for bugs." ); + } + } + } + + if ( "maintainers" in manifest ) { + if ( !Array.isArray( manifest.maintainers ) ) { + errors.push( "Invalid data type for maintainers; must be an array." ); + } else { + manifest.maintainers.forEach(function( maintainer, i ) { + if ( !isObject( maintainer ) ) { + errors.push( "Invalid data type for maintainers[" + i + "]; must be an object." ); + return; + } + + if ( !("name" in maintainer) ) { + errors.push( "Missing required field: maintainers[" + i + "].name." ); + } else if ( typeof maintainer.name !== "string" ) { + errors.push( "Invalid data type for maintainers[" + i + "].name; must be a string." ); + } + + if ( "email" in maintainer ) { + if ( typeof maintainer.email !== "string" ) { + errors.push( "Invalid data type for maintainers[" + i + "].email; must be a string." ); + } else if ( !isEmail( maintainer.email ) ) { + errors.push( "Invalid value for maintainers[" + i + "].email." ); + } + } + + if ( "url" in maintainer ) { + if ( typeof maintainer.url !== "string" ) { + errors.push( "Invalid data type for maintainers[" + i + "].url; must be a string." ); + } else if ( !isUrl( maintainer.url ) ) { + errors.push( "Invalid value for maintainers[" + i + "].url." ); + } + } + }); + } + } + + return errors; +}; + +})( + typeof exports === "object" ? exports : this.Manifest = {}, + this.semver || require( "semver" ) +); \ No newline at end of file diff --git a/lib/pluginsdb.js b/lib/pluginsdb.js index 8bf13cb..9686f8c 100644 --- a/lib/pluginsdb.js +++ b/lib/pluginsdb.js @@ -69,6 +69,11 @@ var pluginsDb = module.exports = { }); }), + transferOwnership: auto(function( plugin, owner, repo, fn ) { + db.run( "UPDATE plugins SET owner = ?, repo = ? WHERE plugin = ?", + [ owner, repo, plugin ], fn ); + }), + getTags: auto(function( repoId, fn ) { db.all( "SELECT tag FROM repos WHERE repo = ?", [ repoId ], function( error, tags ) { if ( error ) { diff --git a/lib/service.js b/lib/service.js index 97be252..d676f62 100644 --- a/lib/service.js +++ b/lib/service.js @@ -1,10 +1,15 @@ -var semver = require( "semver" ), +var fs = require( "fs" ), + semver = require( "semver" ), Step = require( "step" ), config = require( "./config" ), logger = require( "./logger" ), + Manifest = require( "./manifest" ), suites = require( "./suites" ), blacklist = require( "./blacklist" ); +Manifest.suites = suites; +Manifest.blacklist = blacklist; + function extend( a, b ) { for ( var prop in b ) { a[ prop ] = b[ prop ]; @@ -17,35 +22,20 @@ function Repo() { this.path = config.repoDir + "/" + this.id; } -function isObject( obj ) { - return ({}).toString.call( obj ) === "[object Object]"; -} - -function isUrl( /*str*/ ) { - // TODO: URL validation - return true; -} - -function isEmail( str ) { - return (/^[a-zA-Z0-9.!#$%&'*+\/=?\^_`{|}~\-]+@[a-zA-Z0-9\-]+(?:\.[a-zA-Z0-9\-]+)*$/).test( str ); -} - // manifest extend( Repo.prototype, { - getManifest: function( version, file, fn ) { - this._getManifest( version, file, function( error, manifest ) { + getManifest: function( tag, file, fn ) { + var repo = this; + this._getManifest( tag, file, function( error, manifest ) { if ( error ) { return fn( error ); } - if ( !manifest ) { - return fn( null, null ); - } - try { manifest = JSON.parse( manifest ); } catch( error ) { - // TODO: report error to user? + logger.log( "Invalid JSON in manifest", repo.id, tag, file ); + repo.informInvalidJson({ tag: tag, file: file }); return fn( null, null ); } @@ -54,221 +44,7 @@ extend( Repo.prototype, { }, validateManifest: function( manifest, version, prefix, filename ) { - var errors = []; - - /** required fields **/ - - if ( !manifest.name ) { - errors.push( "Missing required field: name." ); - } else if ( typeof manifest.name !== "string" ) { - errors.push( "Invalid data type for name; must be a string." ); - } else if ( !(/^[a-zA-Z0-9_\.\-]+$/).test( manifest.name ) ) { - errors.push( "Name contains invalid characters." ); - } else if ( blacklist.indexOf( manifest.name ) !== -1 ) { - errors.push( "Name must not be '" + manifest.name + "'." ); - } else { - if ( prefix ) { - if ( manifest.name.indexOf( prefix ) !== 0 ) { - errors.push( "Name must start with '" + prefix + "'." ); - } - } else { - Object.keys( suites ).forEach(function( repoId ) { - var prefix = suites[ repoId ]; - if ( manifest.name.indexOf( prefix ) === 0 && - !(/\./).test( manifest.name.substr( prefix.length ) ) ) { - errors.push( "Name must not start with '" + prefix + "'." ); - } - }); - } - - if ( filename && filename.substr( 0, filename.length - 12 ) !== manifest.name ) { - errors.push( "Name must match manifest file name." ); - } - } - - if ( !manifest.version ) { - errors.push( "Missing required field: version." ); - } else if ( typeof manifest.version !== "string" ) { - errors.push( "Invalid data type for version; must be a string." ); - } else if ( manifest.version !== semver.clean( manifest.version ) ) { - errors.push( "Manifest version (" + manifest.version + ") is invalid." ); - } else if ( manifest.version !== semver.clean( version ) ) { - errors.push( "Manifest version (" + manifest.version + ") does not match tag (" + version + ")." ); - } - - if ( !manifest.title ) { - errors.push( "Missing required field: title." ); - } else if ( typeof manifest.title !== "string" ) { - errors.push( "Invalid data type for title; must be a string." ); - } - - if ( !manifest.author ) { - errors.push( "Missing required field: author." ); - } else if ( !isObject( manifest.author ) ) { - errors.push( "Invalid data type for author; must be an object." ); - } else if ( !manifest.author.name ) { - errors.push( "Missing required field: author.name." ); - } else { - if ( typeof manifest.author.name !== "string" ) { - errors.push( "Invalid data type for author.name; must be a string." ); - } - - if ( "email" in manifest.author ) { - if ( typeof manifest.author.email !== "string" ) { - errors.push( "Invalid data type for author.email; must be a string." ); - } else if ( !isEmail( manifest.author.email ) ) { - errors.push( "Invalid value for author.email." ); - } - } - - if ( "url" in manifest.author ) { - if ( typeof manifest.author.url !== "string" ) { - errors.push( "Invalid data type for author.url; must be a string." ); - } else if ( !isUrl( manifest.author.url ) ) { - errors.push( "Invalid value for author.url." ); - } - } - } - - if ( !manifest.licenses ) { - errors.push( "Missing required field: licenses." ); - } else if ( !Array.isArray( manifest.licenses ) ) { - errors.push( "Invalid data type for licenses; must be an array." ); - } else if ( !manifest.licenses.length ) { - errors.push( "There must be at least one license." ); - } else { - manifest.licenses.forEach(function( license, i ) { - if ( !license.url ) { - errors.push( "Missing required field: licenses[" + i + "].url." ); - } else if ( typeof license.url !== "string" ) { - errors.push( "Invalid data type for licenses[" + i + "].url; must be a string." ); - } else if ( !isUrl( license.url ) ) { - errors.push( "Invalid value for license.url." ); - } - }); - } - - if ( !manifest.dependencies ) { - errors.push( "Missing required field: dependencies." ); - } else if ( !isObject( manifest.dependencies ) ) { - errors.push( "Invalid data type for dependencies; must be an object." ); - } else { - if ( !manifest.dependencies.jquery ) { - errors.push( "Missing required dependency: jquery." ); - } - Object.keys( manifest.dependencies ).forEach(function( dependency ) { - if ( typeof manifest.dependencies[ dependency ] !== "string" ) { - errors.push( "Invalid data type for dependencies[" + dependency + "];" + - " must be a string." ); - } else if ( !semver.validRange( manifest.dependencies[ dependency ] ) ) { - errors.push( "Invalid version range for dependency: " + dependency + "." ); - } - }); - } - - /** optional fields **/ - - if ( "description" in manifest && typeof manifest.description !== "string" ) { - errors.push( "Invalid data type for description; must be a string." ); - } - - if ( "keywords" in manifest ) { - if ( !Array.isArray( manifest.keywords ) ) { - errors.push( "Invalid data type for keywords; must be an array." ); - } else { - manifest.keywords.forEach(function( keyword, i ) { - if ( typeof keyword !== "string" ) { - errors.push( "Invalid data type for keywords[" + i + "]; must be a string." ); - } else if ( !(/^[a-zA-Z0-9\.\-]+$/).test( keyword ) ) { - errors.push( "Invalid characters for keyword: " + keyword + "." ); - } - }); - } - } - - if ( "homepage" in manifest ) { - if ( typeof manifest.homepage !== "string" ) { - errors.push( "Invalid data type for homepage; must be a string." ); - } else if ( !isUrl( manifest.homepage ) ) { - errors.push( "Invalid value for homepage." ); - } - } - - if ( "docs" in manifest ) { - if ( typeof manifest.docs !== "string" ) { - errors.push( "Invalid data type for docs; must be a string." ); - } else if ( !isUrl( manifest.docs ) ) { - errors.push( "Invalid value for docs." ); - } - } - - if ( "demo" in manifest ) { - if ( typeof manifest.demo !== "string" ) { - errors.push( "Invalid data type for demo; must be a string." ); - } else if ( !isUrl( manifest.demo ) ) { - errors.push( "Invalid value for demo." ); - } - } - - if ( "download" in manifest ) { - if ( typeof manifest.download !== "string" ) { - errors.push( "Invalid data type for download; must be a string." ); - } else if ( !isUrl( manifest.download ) ) { - errors.push( "Invalid value for download." ); - } - } - - if ( "bugs" in manifest ) { - // check { url: "..." } format - if ( typeof manifest.bugs === "object" ) { - if ( typeof manifest.bugs.url !== "string" ) { - errors.push( "Invalid data type for bugs.url; must be a string." ); - } else if ( !isUrl( manifest.bugs.url ) ) { - errors.push( "Invalid value for bugs.url." ); - } - } else { - if ( typeof manifest.bugs !== "string" ) { - errors.push( "Invalid data type for bugs; must be a string." ); - } else if ( !isUrl( manifest.bugs ) ) { - errors.push( "Invalid value for bugs." ); - } - } - } - - if ( "maintainers" in manifest ) { - if ( !Array.isArray( manifest.maintainers ) ) { - errors.push( "Invalid data type for maintainers; must be an array." ); - } else { - manifest.maintainers.forEach(function( maintainer, i ) { - if ( !isObject( maintainer ) ) { - errors.push( "Invalid data type for maintainers[" + i + "]; must be an object." ); - return; - } - - if ( !("name" in maintainer) ) { - errors.push( "Missing required field: maintainers[" + i + "].name." ); - } else if ( typeof maintainer.name !== "string" ) { - errors.push( "Invalid data type for maintainers[" + i + "].name; must be a string." ); - } - - if ( "email" in maintainer ) { - if ( typeof maintainer.email !== "string" ) { - errors.push( "Invalid data type for maintainers[" + i + "].email; must be a string." ); - } else if ( !isEmail( maintainer.email ) ) { - errors.push( "Invalid value for maintainers[" + i + "].email." ); - } - } - - if ( "url" in maintainer ) { - if ( typeof maintainer.url !== "string" ) { - errors.push( "Invalid data type for maintainers[" + i + "].url; must be a string." ); - } else if ( !isUrl( maintainer.url ) ) { - errors.push( "Invalid value for maintainers[" + i + "].url." ); - } - } - }); - } - } + var errors = Manifest.validate( manifest, version, prefix, filename ); if ( errors.length ) { logger.log( "Manifest errors:", this.id, version, filename, errors ); @@ -289,6 +65,11 @@ extend( Repo.prototype, { function( error, tags ) { if ( error ) { + if ( /Repository not found/.test( error.message ) ) { + repo.informRepoNotFound(); + return fn( null, [] ); + } + return fn( error ); } @@ -344,6 +125,7 @@ extend( Repo.prototype, { if ( !files.length ) { logger.log( "No manifest files for", repo.id, tag ); + repo.informMissingManifset({ tag: tag }); return fn( null, null ); } @@ -358,7 +140,7 @@ extend( Repo.prototype, { // validate manifests function( error, files, manifests ) { - var i, l, + var i, l, errors, mappedManifests = {}; if ( error ) { @@ -373,8 +155,14 @@ extend( Repo.prototype, { } for ( i = 0, l = manifests.length; i < l; i++ ) { - if ( repo.validateManifest( manifests[ i ], tag, - suites[ repo.id ], files[ i ] ).length ) { + errors = repo.validateManifest( manifests[ i ], tag, + suites[ repo.id ], files[ i ] ); + if ( errors.length ) { + repo.informInvalidManifest({ + tag: tag, + file: files[ i ], + errors: errors + }); return fn( null, null ); } mappedManifests[ files[ i ] ] = manifests[ i ]; @@ -386,6 +174,31 @@ extend( Repo.prototype, { } }); +// Status notifications +extend( Repo.prototype, { + inform: function( msg ) { + fs.appendFile( config.errorLog, (new Date()).toGMTString() + " " + msg + "\n" ); + }, + informMissingManifset: function( data ) { + this.inform( this.id + " " + data.tag + " has no manifest file(s)." ); + }, + informInvalidJson: function( data ) { + this.inform( this.id + " " + data.tag + " " + data.file + " is invalid JSON." ); + }, + informInvalidManifest: function( data ) { + this.inform( this.id + " " + data.tag + " " + data.file + " has the following errors: " + data.errors ); + }, + informOtherOwner: function( data ) { + this.inform( this.id + " " + data.tag + " cannot publish " + data.name + " which is owned by " + data.owner ); + }, + informRepoNotFound: function() { + this.inform( this.id + " repo not found on remote server." ); + }, + informSuccess: function( data ) { + this.inform( this.id + " SUCCESSFULLY ADDED " + data.name + " v" + data.version + "!" ); + } +}); + var services = {}; module.exports = { diff --git a/lib/service/github.js b/lib/service/github.js index a2caf4e..a616667 100644 --- a/lib/service/github.js +++ b/lib/service/github.js @@ -110,22 +110,19 @@ extend( GithubRepo.prototype, { _getManifest: function( version, file, fn ) { version = version || "master"; - exec( "git show " + version + ":" + file, { cwd: this.path }, function( error, stdout, stderr ) { - // this will also result in an error being passed, so we check stderr first - if ( stderr && stderr.substring( 0, 11 ) === "fatal: Path" ) { - return fn( null, null ); - } - + exec( "git show " + version + ":" + file, { cwd: this.path }, function( error, stdout ) { if ( error ) { return fn( error ); } - fn( null, stdout ); + fn( null, stdout.trim() ); }); }, getReleaseDate: function( tag, fn ) { - exec( "git log --pretty='%cD' -1 " + tag, { cwd: this.path }, function( error, stdout ) { + // The trailing "--" avoids an ambiguous argument in case a repo + // contains a path that matches the tag name + exec( "git log --pretty='%cD' -1 " + tag + " --", { cwd: this.path }, function( error, stdout ) { if ( error ) { return fn( error ); } @@ -163,7 +160,8 @@ extend( GithubRepo.prototype, { function( error ) { // repo already exists if ( !error ) { - return exec( "git fetch -t", { cwd: repo.path }, this ); + exec( "git fetch -t", { cwd: repo.path }, this ); + return; } // error other than repo not existing diff --git a/lib/suites.json b/lib/suites.json index 797e3c8..cecb6cf 100644 --- a/lib/suites.json +++ b/lib/suites.json @@ -1,3 +1,4 @@ { - "github/jquery/jquery-ui": "ui." + "github/jquery/jquery-ui": "ui.", + "github/acidb/mobiscroll": "mobiscroll." } diff --git a/package.json b/package.json index c8f96d5..088f01f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "plugins.jquery.com", - "version": "1.0.8", + "version": "1.3.1", "author": "jQuery Project", "description": "The official jQuery plugins site", "homepage": "https://github.com/jquery/plugins.jquery.com", @@ -11,15 +11,14 @@ "dependencies": { "mkdirp": "0.3.4", "semver": "1.1.2", - "sqlite3": "2.1.5", + "sqlite3": "2.1.7", "step": "0.0.5", "rimraf": "2.1.1", "wordpress": "0.1.3", "grunt": "0.3.17", - "grunt-wordpress": "1.0.5", + "grunt-wordpress": "1.2.1", "grunt-check-modules": "0.1.0", - "grunt-jquery-content": "0.8.1", - "grunt-clean": "0.3.0", - "simple-log": "1.0.1" + "grunt-jquery-content": "0.13.0", + "simple-log": "1.1.0" } } diff --git a/pages/docs/names.md b/pages/docs/names.md index c1d0295..00263fd 100644 --- a/pages/docs/names.md +++ b/pages/docs/names.md @@ -7,7 +7,7 @@ your plugin. The name is a unique identifier that distinguishes your plugin from all other plugins. This is different from the title of your plugin, which you can think of as the display name. -**Plugin names may only contain letters, numbers, hypens, dots, and underscores.** +**Plugin names may only contain letters, numbers, hyphens, dots, and underscores.** We encourage you to follow a few simple tips as well: diff --git a/pages/docs/package-manifest.md b/pages/docs/package-manifest.md index b815e2c..8a41b02 100644 --- a/pages/docs/package-manifest.md +++ b/pages/docs/package-manifest.md @@ -16,25 +16,25 @@ The files must be actual JSON, not just a JavaScript object literal. ### Required Fields -* name -* version -* title -* author -* licenses -* dependencies +* name +* version +* title +* author +* licenses +* dependencies ### Optional Fields -* description -* keywords -* homepage -* docs -* demo -* download -* bugs -* maintainers +* description +* keywords +* homepage +* docs +* demo +* download +* bugs +* maintainers -### name +### name The *most* important things in your manifest file are the name and version fields. The name and version together form an identifier that is assumed @@ -54,7 +54,7 @@ to see if there's something by that name already, before you get too attached to Site, either consider renaming your plugin or namespacing it. For example, jQuery UI plugins are listed with the "ui." prefix (e.g. ui.dialog, ui.autocomplete). -### version +### version The *most* important things in your manifest file are the name and version fields. The name and version together form an identifier that is assumed @@ -64,33 +64,33 @@ per [node-semver](https://github.com/isaacs/node-semver). See [Specifying Versions](#specifying-versions). -### title +### title A nice complete and pretty title of your plugin. This will be used for the page title and top-level heading on your plugin's page. Include jQuery (if you want) and -spaces and mixed case, unlike [name](#field-name). +spaces and mixed case, unlike [name](#name). -### author +### author One person. See [People Fields](#people-fields). -### licenses +### licenses Array of licenses under which the plugin is provided. Each license is a hash with a url property linking to the actual text and an optional "type" property specifying the type of license. If the license is one of the [official open source licenses](http://www.opensource.org/licenses/alphabetical), the official license name or its abbreviation may be explicated with the "type" property. ``` "licenses": [ - { - "type": "GPLv2", - "url": "http://www.example.com/licenses/gpl.html" - } + { + "type": "GPLv2", + "url": "http://www.example.com/licenses/gpl.html" + } ] ``` -### dependencies +### dependencies Dependencies are specified with a simple hash of package name to version range. The version range is EITHER a string which has one or more @@ -106,55 +106,55 @@ of each library you depend on. You must list at least one dependency, `jquery` (note that it's lower-case). -### description +### description Put a description in it. It's a string. This helps people discover your plugin, as it's listed on the jQuery Plugins Site. -### keywords +### keywords Put keywords in it. It's an array of strings. This helps people discover your plugin as it's listed on the jQuery Plugins Site. Keywords may only contain letters, numbers, hyphens, and dots. -### homepage +### homepage The url to the plugin homepage. -### docs +### docs The url to the plugin documentation. -### demo +### demo The url to the plugin demo or demos. -### download +### download The url to download the plugin. A download URL will be automatically generated based on the tag in GitHub, but you can specify a custom URL if you'd prefer to send users to your own site. -### bugs +### bugs The url to the bug tracker for the plugin. -### maintainers +### maintainers An array of people. See [People Fields](#people-fields). -## People Fields +## People Fields A "person" is an object with a "name" field and optionally "url" and "email", like this: -``` json +```json { - "name" : "Barney Rubble", - "email" : "b@rubble.com", - "url" : "http://barnyrubble.tumblr.com/" + "name" : "Barney Rubble", + "email" : "b@rubble.com", + "url" : "http://barnyrubble.tumblr.com/" } ``` @@ -162,7 +162,7 @@ Both the email and url are optional. --- -## Specifying Versions +## Specifying Versions Version range descriptors may be any of the following styles, where "version" is a semver compatible version identifier. @@ -176,7 +176,6 @@ is a semver compatible version identifier. * `~version` See 'Tilde Version Ranges' below * `1.2.x` See 'X Version Ranges' below * `*` Matches any version -* `""` (just an empty string) Same as `*` * `version1 - version2` Same as `>=version1 <=version2`. * `range1 || range2` Passes if either range1 or range2 are satisfied. @@ -184,22 +183,22 @@ For example, these are all valid: ```json { "dependencies" : - { - "foo" : "1.0.0 - 2.9999.9999", - "bar" : ">=1.0.2 <2.1.2", - "baz" : ">1.0.2 <=2.3.4", - "boo" : "2.0.1", - "qux" : "<1.0.0 || >=2.3.1 <2.4.5 || >=2.5.2 <3.0.0", - "asd" : "http://asdf.com/asdf.tar.gz", - "til" : "~1.2", - "elf" : "~1.2.3", - "two" : "2.x", - "thr" : "3.3.x" - } + { + "foo" : "1.0.0 - 2.9999.9999", + "bar" : ">=1.0.2 <2.1.2", + "baz" : ">1.0.2 <=2.3.4", + "boo" : "2.0.1", + "qux" : "<1.0.0 || >=2.3.1 <2.4.5 || >=2.5.2 <3.0.0", + "asd" : "http://asdf.com/asdf.tar.gz", + "til" : "~1.2", + "elf" : "~1.2.3", + "two" : "2.x", + "thr" : "3.3.x" + } } ``` -### Tilde Version Ranges +### Tilde Version Ranges A range specifier starting with a tilde `~` character is matched against a version in the following fashion. @@ -210,10 +209,10 @@ a version in the following fashion. For example, the following are equivalent: * `"~1.2.3" = ">=1.2.3 <1.3.0"` -* `"~1.2" = ">=1.2.0 <2.0.0"` +* `"~1.2" = ">=1.2.0 <1.3.0"` * `"~1" = ">=1.0.0 <2.0.0"` -### X Version Ranges +### X Version Ranges An "x" in a version range specifies that the version number must start with the supplied digits, but any digit may be used in place of the x. @@ -229,44 +228,43 @@ The following are equivalent: You may not supply a comparator with a version containing an x. Any digits after the first "x" are ignored. -### Sample manifest +### Sample manifest **color.jquery.json** ```json { - "name": "color", - "title": "jQuery Color", - "description": "jQuery plugin for color manipulation and animation support.", - "keywords": [ - "color", - "animation" - ], - "version": "2.1.2", - "author": { - "name": "jQuery Foundation and other contributors", - "url": "https://github.com/jquery/jquery-color/blob/2.1.2/AUTHORS.txt" - }, - "maintainers": [ - { - "name": "Corey Frang", - "email": "gnarf37@gmail.com", - "url": "http://gnarf.net" - } - ], - "licenses": [ - { - "type": "MIT", - "url": "https://github.com/jquery/jquery-color/blob/2.1.2/MIT-LICENSE.txt" - } - ], - "bugs": "https://github.com/jquery/jquery-color/issues", - "homepage": "https://github.com/jquery/jquery-color", - "docs": "https://github.com/jquery/jquery-color", - "download": "http://code.jquery.com/#color", - "dependencies": { - "jquery": ">=1.5" - } + "name": "color", + "title": "jQuery Color", + "description": "jQuery plugin for color manipulation and animation support.", + "keywords": [ + "color", + "animation" + ], + "version": "2.1.2", + "author": { + "name": "jQuery Foundation and other contributors", + "url": "https://github.com/jquery/jquery-color/blob/2.1.2/AUTHORS.txt" + }, + "maintainers": [ + { + "name": "Corey Frang", + "email": "gnarf37@gmail.com", + "url": "http://gnarf.net" + } + ], + "licenses": [ + { + "type": "MIT", + "url": "https://github.com/jquery/jquery-color/blob/2.1.2/MIT-LICENSE.txt" + } + ], + "bugs": "https://github.com/jquery/jquery-color/issues", + "homepage": "https://github.com/jquery/jquery-color", + "docs": "https://github.com/jquery/jquery-color", + "download": "http://code.jquery.com/#color", + "dependencies": { + "jquery": ">=1.5" + } } ``` - diff --git a/pages/docs/publish.md b/pages/docs/publish.md index 0a0f4fc..b3b3cb5 100644 --- a/pages/docs/publish.md +++ b/pages/docs/publish.md @@ -1,29 +1,48 @@ +
+ The jQuery Plugin Registry is in read-only mode. New plugin releases will not be processed. We recommend moving to npm, using "jquery-plugin" as the keyword in your package.json. The npm blog has instructions for publishing your plugin to npm. +
+ Publishing your plugin on the site is a three step process: -## Add a Post-Receive Hook +## Add a Service Hook -First, you'll need to create a post-receive hook on GitHub. Just follow the -[step-by-step guide for adding a -webhook](https://help.github.com/articles/post-receive-hooks) and set the URL -to `http://plugins.jquery.com/postreceive-hook`. +First, you'll need to enable the jQuery Plugins service hook on GitHub. On the +settings page for your repository, click the Webhooks & Services link, then +click the Configure services button. Scroll down to find the jQuery Plugins +service and enable it (there's no config, just check the Active checkbox and +click the Update settings button). ## Add a Manifest to your Repository The jQuery Plugins Registry will look in the root level of your repository for -any files named `*.jquery.json`. You will want to create -yourplugin.jquery.json according to the [package manifest -specification](/docs/package-manifest/). You are now ready to publish your -plugin! +any files named `*.jquery.json`. You will want to create +`*yourplugin*.jquery.json` according to the [package manifest +specification](/docs/package-manifest/). Use an online JSON verifier such as +[JSONlint](http://jsonlint.com) to make sure the file is valid. You are now +ready to publish your plugin! + +## Validate Your Manifest File Here + +
+ Upload your manifest file to check for common errors: + +

Since this tool uses the new HTML5 FileReader API to look at the file contents + without actually uploading your file to the server, you'll need a modern browser + like Chrome, Safari, Firefox, Opera or IE10.

+

+
+ + ## Publishing a Version -After the post-receive hook is setup and your manifest has been added, +After the service hook is setup and your manifest has been added, publishing your plugin is as simple as tagging the version in git and pushing -the tag to GitHub. The post-receive hook will notify the plugins site that a +the tag to GitHub. The service hook will notify the plugins site that a new tag is available and the plugins site will take care of the rest! ```bash @@ -31,23 +50,29 @@ $ git tag 0.1.0 $ git push origin --tags ``` -The name of the tag **must** be a valid [semver](http://semver.org/) value. The -tag name may contain an optional `v` prefix. The tag name must also match the -version listed in the manifest file. If the manifest file is valid, then the -version will be automatically added to the plugins site. +The name of the tag **must** be a valid [semver](http://semver.org/) value, but +may contain an optional `v` prefix. The tag name must also match the +version listed in the manifest file. So, if the version field in the manifest +is "0.1.1" the tag should be either "0.1.1" or "v0.1.1". If the manifest file +is valid, the version will be automatically added to the plugins site. + +The registry **does not support re-processing tags that it has already seen.** +Therefore, we strongly suggest that you **do not overwrite old tags**. Instead, +update the version number tag in the manifest, commit, and create a new tag to +fix any errors you've encountered. + +For example, you've pushed version `v1.7.0` of your plugin, but there is an +[error detected](/error.log) in the manifest. If you fix the error, delete, +re-create, and push another `v1.7.0` tag, the registry **will not** detect it. +You will have to create and push `v1.7.1`. -We highly suggest that you **do not overwrite old tags**, instead, push a new -version number tag (and commit to the manifest) to fix any errors you've -encounterd. -## Having Trouble? +## Troubleshooting -Unfortunately we do not currently have a system for -notifying you if there is a problem. If you're interested in helping improve -this aspect of the plugins site, we'd [love your -help](https://github.com/jquery/plugins.jquery.com/issues/11). +If you have problems with your plugin not publishing you should check the +[error log](/error.log) for hints on what the problem might be. -If you encounter trouble getting this process to work with your plugin, please +If you still encounter trouble getting this process to work with your plugin, please join the IRC channel [#jquery-content](irc://freenode.net:6667/#jquery-content) on [freenode](http://freenode.net). If you can't seem to connect with someone in the IRC channel, please feel free to email us at diff --git a/resources/validate.js b/resources/validate.js new file mode 100644 index 0000000..1807392 --- /dev/null +++ b/resources/validate.js @@ -0,0 +1,36 @@ +$(function() { + var output = $( "#validator-output" ); + function log( msg ) { + output.text( msg ); + } + + $( "input[name='files']" ).on( "change", function( event ) { + event.preventDefault(); + var reader = new FileReader(), + files = event.target.files, + file = files && files[0]; + + if ( !file ) { + return; + } + + reader.onload = function( event ) { + var manifest, errors; + try { + manifest = $.parseJSON( event.target.result ); + } catch( error ) { + return log( "Your manifest file contains invalid JSON." ); + } + + errors = Manifest.validate( manifest, null, null, file.name ); + if ( errors.length ) { + log( "Your manifest file contains the following errors:\n\n" + + errors.join( "\n" ) ); + } else { + log( "Congratulations, your manifest file is valid." ); + } + }; + + reader.readAsText( file ); + }); +}); diff --git a/scripts/manager.js b/scripts/manager.js index 7e3eaf2..d2373fd 100644 --- a/scripts/manager.js +++ b/scripts/manager.js @@ -1,7 +1,12 @@ +function wait() { + setTimeout( wait, 1000 ); +} +return wait(); + var path = require( "path" ), spawn = require( "child_process" ).spawn, logger = require( "../lib/logger" ), - consoleOption = process.argv.indexOf( "--console" ) ? "--console" : ""; + consoleOption = process.argv.indexOf( "--console" ) !== -1 ? "--console" : ""; logger.log( "Manager started." ); diff --git a/scripts/retry.js b/scripts/retry.js index cbe5fde..64c0b4d 100644 --- a/scripts/retry.js +++ b/scripts/retry.js @@ -87,6 +87,9 @@ var processFailures = function( fn ) { processFailures(function( error ) { if ( error ) { logger.error( "Error during retry: " + error.stack ); + + // Kill the process with an error code and let the manager restart it + process.exit( 1 ); } }); diff --git a/scripts/update-server.js b/scripts/update-server.js index 9ce82c6..939a32a 100644 --- a/scripts/update-server.js +++ b/scripts/update-server.js @@ -1,4 +1,6 @@ var http = require( "http" ), + fs = require( "fs" ), + config = require( "../lib/config" ), service = require( "../lib/service" ), hook = require( "../lib/hook" ), logger = require( "../lib/logger" ), @@ -23,6 +25,10 @@ var server = http.createServer(function( request, response ) { }); request.on( "end", function() { + if ( request.url === "/error.log" ) { + return fs.createReadStream( config.errorLog ).pipe( response ); + } + var repo = service.getRepoByHook( data ); if ( !repo ) { diff --git a/scripts/wordpress-update.js b/scripts/wordpress-update.js index 50df195..5abf49a 100644 --- a/scripts/wordpress-update.js +++ b/scripts/wordpress-update.js @@ -16,6 +16,13 @@ process.on( "uncaughtException", function( error ) { function isStable( version ) { return (/^\d+\.\d+\.\d+$/).test( version ); } +function extend( a, b ) { + for ( var p in b ) { + a[ p ] = b[ p ]; + } + + return a; +} var actions = {}; @@ -134,7 +141,10 @@ actions.addRelease = function( data, fn ) { // main page is constructed from the new version since pretty much // anything can change between versions. if ( versions.latest === manifest.version ) { - mainPage = Object.create( pageDetails ); + extend( mainPage, pageDetails ); + // don't update the post date on main page, and let it be set + // to whenever we processed it, no harm here. + delete mainPage.date; mainPage.name = manifest.name; mainPage.customFields = mergeCustomFields( existingCustomFields, pageDetails.customFields ); @@ -285,6 +295,9 @@ function processNextAction( actionId, fn ) { processActions(function( error ) { if ( error ) { logger.error( "Error updating WordPress: " + error.stack ); + + // Kill the process with an error code and let the manager restart it + process.exit( 1 ); } }); diff --git a/test/service.js b/test/service.js index d0c3052..110cd45 100644 --- a/test/service.js +++ b/test/service.js @@ -209,6 +209,11 @@ var tests = { // ]); // }, + "dependencies - infinite version range": function( manifest, fn ) { + manifest.dependencies.jquery = "*"; + fn( manifest, manifest.version, [] ); + }, + "dependencies - invalid type": function( manifest, fn ) { manifest.dependencies = [ "jquery" ]; fn( manifest, manifest.version, [