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 8ed5d67..6716162 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ config.json last-action plugins.db retry.db -node_modules \ No newline at end of file +error.log +node_modules +dist/ diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..325f247 --- /dev/null +++ b/.jshintrc @@ -0,0 +1,13 @@ +{ + "curly": true, + "eqnull": true, + "eqeqeq": true, + "expr": true, + "noarg": true, + "node": true, + "onevar": true, + "trailing": true, + "undef": true, + "unused": true, + "strict": false +} 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 bce669e..f898df8 100644 --- a/README.md +++ b/README.md @@ -4,18 +4,35 @@ 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 jquery.json manifest files in the repository root. The specification for this file is in [docs/manifest.md](/jquery/plugins.jquery.com/blob/master/docs/manifest.md). +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/_update`. -**Warning:** This is not yet functional! +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 ### Requires -* jQuery's [web-base-template](https://github.com/jquery/web-base-template) +* jQuery's [jquery-wp-content](https://github.com/jquery/jquery-wp-content/) * Web server (such as Apache) * PHP * MySQL @@ -27,27 +44,57 @@ Simply add a [post-receive hook](http://help.github.com/post-receive-hooks/) to #### web-base-template -1. Follow the installation steps for [web-base-template](https://github.com/jquery/web-base-template). +1. Follow the installation steps for [jquery-wp-content](https://github.com/jquery/jquery-wp-content/). #### Install node >=0.6.4 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 17f8d32..9dbff99 100644 --- a/grunt.js +++ b/grunt.js @@ -1,20 +1,40 @@ -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-jquery-content" ); +grunt.loadNpmTasks( "grunt-check-modules" ); grunt.initConfig({ lint: { grunt: "grunt.js", src: [ "lib/**", "scripts/**" ] }, - + jshint: { + grunt: { options: grunt.file.readJSON( ".jshintrc" ) }, + src: { options: grunt.file.readJSON( ".jshintrc" ) } + }, 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 ) +}); - wordpress: config.wordpress +grunt.registerTask( "clean", function() { + rimraf.sync( "dist" ); }); // We only want to sync the documentation, so we override wordpress-get-postpaths @@ -34,37 +54,81 @@ grunt.registerHelper( "wordpress-get-postpaths", function( fn ) { }); }); -grunt.registerTask( "docs", function() { - var done = this.async(); - grunt.helper( "wordpress-sync-posts", "site-content/", function( error ) { +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(), + 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 ) { - done( false ); + return done( false ); } done(); }); }); +// 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 ); // clean pluginsDb rimraf.sync( config.pluginsDb ); - rimraf.sync( "last-action" ); + rimraf.sync( config.lastActionFile ); // clean retrydb rimraf.sync( retry.dbPath ); }); -grunt.registerTask( "clean", function() { +// clean-retries will only delete information about retries. It will not delete the +// plugin registry and it will not remove local clones. This is useful for +// restoring a WordPress site on a server that already has repos. +grunt.registerTask( "clean-retries", function() { var rimraf = require( "rimraf" ), retry = require( "./lib/retrydb" ); - rimraf.sync( "last-action" ); + rimraf.sync( config.lastActionFile ); rimraf.sync( retry.dbPath ); }); @@ -108,9 +172,12 @@ grunt.registerTask( "restore-repos", function() { }); }); + + grunt.registerTask( "default", "lint test" ); -grunt.registerTask( "setup", "setup-pluginsdb setup-retrydb docs" ); -grunt.registerTask( "update", "docs" ); -grunt.registerTask( "restore", "clean setup-retrydb 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 99bb738..72c2f59 100644 --- a/lib/config.js +++ b/lib/config.js @@ -11,5 +11,7 @@ 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 2a545ec..954fd05 100644 --- a/lib/hook.js +++ b/lib/hook.js @@ -1,5 +1,4 @@ -var semver = require( "semver" ), - Step = require( "step" ), +var Step = require( "step" ), pluginsDb = require( "./pluginsdb" ), retry = require( "./retrydb" ), logger = require( "./logger" ); @@ -55,9 +54,11 @@ function processVersions( repo, fn ) { } if ( !tags.length ) { + logger.log( "No tags to process for " + repo.id ); return fn( null ); } + logger.log( "Processing", repo.id, tags ); this.parallel()( null, tags ); var group = this.group(); tags.forEach(function( tag ) { @@ -93,6 +94,7 @@ function processVersions( repo, fn ) { } if ( !releases.length ) { + logger.log( "No valid releases for " + repo.id ); return fn( null ); } @@ -127,16 +129,29 @@ 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; }, // track the new release - function( error, owner ) { + function( /*error, owner*/ ) { pluginsDb.addRelease( repo.id, tag, file, manifest, this ); }, @@ -147,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/logger.js b/lib/logger.js index 1f6897c..75e4d37 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -1 +1 @@ -module.exports = require( "logger" ).init( "plugins.jquery.com" ); +module.exports = require( "simple-log" ).init( "plugins.jquery.com" ); 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/retrydb.js b/lib/retrydb.js index 2ce87ca..d045350 100644 --- a/lib/retrydb.js +++ b/lib/retrydb.js @@ -88,7 +88,7 @@ module.exports = { var Step = require( "step" ); Step( - function( error ) { + function() { connect( this ); }, diff --git a/lib/service.js b/lib/service.js index 4a7385f..d676f62 100644 --- a/lib/service.js +++ b/lib/service.js @@ -1,9 +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 ]; @@ -16,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 ); } @@ -53,211 +44,10 @@ 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 ) { - 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." ); - } - } + var errors = Manifest.validate( manifest, version, prefix, filename ); - 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." ); - } - } - }); - } + if ( errors.length ) { + logger.log( "Manifest errors:", this.id, version, filename, errors ); } return errors; @@ -275,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 ); } @@ -329,9 +124,12 @@ extend( Repo.prototype, { } if ( !files.length ) { + logger.log( "No manifest files for", repo.id, tag ); + repo.informMissingManifset({ tag: tag }); return fn( null, null ); } + logger.log( "Found manifest files for", repo.id, tag, files ); this.parallel()( null, files ); var group = this.group(); @@ -342,7 +140,8 @@ extend( Repo.prototype, { // validate manifests function( error, files, manifests ) { - var mappedManifests = {}; + var i, l, errors, + mappedManifests = {}; if ( error ) { return fn( error ); @@ -355,9 +154,15 @@ extend( Repo.prototype, { return fn( null, null ); } - for ( var i = 0, l = manifests.length; i < l; i++ ) { - if ( repo.validateManifest( manifests[ i ], tag, - suites[ repo.id ], files[ i ] ).length ) { + for ( i = 0, l = manifests.length; i < l; i++ ) { + 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 ]; @@ -369,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 c4cd874..a616667 100644 --- a/lib/service/github.js +++ b/lib/service/github.js @@ -1,7 +1,6 @@ var fs = require( "fs" ), querystring = require( "querystring" ), exec = require( "child_process" ).exec, - semver = require( "semver" ), Step = require( "step" ), mkdirp = require( "mkdirp" ), service = require( "../service" ); @@ -97,7 +96,7 @@ extend( GithubRepo.prototype, { }, getManifestFiles: function( tag, fn ) { - exec( "git ls-tree " + tag + " --name-only", { cwd: this.path }, function( error, stdout, stderr ) { + exec( "git ls-tree " + tag + " --name-only", { cwd: this.path }, function( error, stdout ) { if ( error ) { return fn( error ); } @@ -111,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 ); } @@ -164,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 d84b2f6..cecb6cf 100644 --- a/lib/suites.json +++ b/lib/suites.json @@ -1,3 +1,4 @@ { - "github/rdworth/temp-jqueryui": "ui." + "github/jquery/jquery-ui": "ui.", + "github/acidb/mobiscroll": "mobiscroll." } diff --git a/package.json b/package.json index 857ec41..088f01f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "plugins.jquery.com", - "version": "0.0.0", + "version": "1.3.1", "author": "jQuery Project", "description": "The official jQuery plugins site", "homepage": "https://github.com/jquery/plugins.jquery.com", @@ -9,14 +9,16 @@ "url": "git://github.com/jquery/plugins.jquery.com.git" }, "dependencies": { - "mkdirp": "0.3.3", - "semver": "1.0.14", - "sqlite3": "2.1.5", + "mkdirp": "0.3.4", + "semver": "1.1.2", + "sqlite3": "2.1.7", "step": "0.0.5", - "rimraf": "2.0.2", + "rimraf": "2.1.1", "wordpress": "0.1.3", - "grunt": "0.3.12", - "grunt-wordpress": "1.0.2", - "logger": "git://github.com/jquery/node-logger.git" + "grunt": "0.3.17", + "grunt-wordpress": "1.2.1", + "grunt-check-modules": "0.1.0", + "grunt-jquery-content": "0.13.0", + "simple-log": "1.1.0" } } diff --git a/site-content/page/docs.html b/pages/docs.html similarity index 100% rename from site-content/page/docs.html rename to pages/docs.html diff --git a/pages/docs/names.md b/pages/docs/names.md new file mode 100644 index 0000000..00263fd --- /dev/null +++ b/pages/docs/names.md @@ -0,0 +1,60 @@ + + +Before you can list your plugin on this site, you'll need to choose a name for +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, hyphens, dots, and underscores.** + +We encourage you to follow a few simple tips as well: + + +* Choose a name that is short, but also reasonably descriptive. +* Match your plugin name to your file name, e.g., the foo plugin would live in a file named jquery.foo.js. +* Check the site to see if the name you want is available, before getting your heart set on a name that's already taken. + +## First Come, First Serve + +Names are registered on a first come, first serve basis. Registering a name +happens automatically the first time you [publish a release](/docs/publish/) of +your plugin. You cannot reserve a name prior to releasing your plugin. Once +you've registered a name, you are the sole owner of that name. Nobody else will +be able to publish a release using the same name. There is no limit on how many +plugins/names a single person may register, but all plugins must be legitimate. + +Package squatting is not allowed. If you sit on a package name and don't +publish code, it may be deleted without warning. + +In these early days of the registry's existence, we do ask that authors reserve +judgment and respect for other popular, widely-adopted plugins that may already +have a reasonable historical claim to a particular name, even if it has not yet +been registered. + +## Transferring Ownership + +While most plugins will only ever have one owner, there are times when the +original owner may move on to other projects and wish to transfer ownership to +someone else. There is currently no automated process for this, the original +owner must contact [plugins@jquery.com](mailto:plugins@jquery.com), prove +ownership and indicate who the new owner should be. + +In the case of an abandoned plugin where the original owner is no longer +active, the jQuery team can choose to change ownership at their discretion. +These will indeed be a rare occurrence, likely requiring an event such as _why +or Mark Pilgrim's infosuicide. + +## Prefixes & Plugin Suites + +Certain prefixes will also be blacklisted for individual plugins. Large +projects which include many plugins in a single repository, such as [jQuery +UI](http://jqueryui.com), are registered as suites. Each suite is required to +have a unique prefix and all of their plugin names must use that prefix. As +such, no other plugin may use a name with a suite's prefix. Suites must be +manually vetted by the jQuery team. + +**Note:** In order to allow proper naming of extensions for plugins in a suite, +the prefix blacklisting is only one level deep. For example, jQuery UI owns all +`ui.*` names, but `ui.autocomplete.*` is open to the public. diff --git a/docs/manifest.md b/pages/docs/package-manifest.md similarity index 57% rename from docs/manifest.md rename to pages/docs/package-manifest.md index 5841df5..8a41b02 100644 --- a/docs/manifest.md +++ b/pages/docs/package-manifest.md @@ -1,9 +1,8 @@ -Specification of the jQuery Plugins Site Manifest File -====================================================== + -# LIVING SPEC (heavily inspired by that of npm, thanks isaacs) - -This document is all you need to know about what's required in your jquery.json +This document is all you need to know about what's required in your `*.jquery.json` manifest file(s). Manifest files must live in the root of your repository and exist in your tags. @@ -11,29 +10,31 @@ The files must be actual JSON, not just a JavaScript object literal. **NOTE: Manifest file names must contain the plugin name, e.g. foo.jquery.json.** -# Fields +--- + +## Fields -## Required Fields +### Required Fields -* name -* version -* title -* author -* licenses -* dependencies +* name +* version +* title +* author +* licenses +* dependencies -## Optional Fields +### 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 @@ -43,17 +44,17 @@ changes to the version. The name is what your thing is called. Some tips: * Don't put "js" or "jquery" in the name. It's assumed that it's js and jquery, since - you're writing a jquery.json manifest file. +you're writing a jquery.json manifest file. * The name ends up being part of a URL. Any name with non-url-safe characters will - be rejected. The jQuery Plugins Site is UTF-8. +be rejected. The jQuery Plugins Site is UTF-8. * The name should be short, but also reasonably descriptive. * You may want to check [the plugins site](http://plugins.jquery.com/) - to see if there's something by that name already, before you get too attached to it. +to see if there's something by that name already, before you get too attached to it. * If you have a plugin with the same name as a plugin already in the jQuery Plugins - 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). +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 @@ -63,31 +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" - } - ] +``` +"licenses": [ + { + "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 @@ -103,59 +106,63 @@ 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: - { - "name" : "Barney Rubble", - "email" : "b@rubble.com", - "url" : "http://barnyrubble.tumblr.com/" - } +```json +{ + "name" : "Barney Rubble", + "email" : "b@rubble.com", + "url" : "http://barnyrubble.tumblr.com/" +} +``` 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. @@ -169,28 +176,29 @@ 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. For example, these are all valid: - { "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" - } - } - -## Tilde Version Ranges +```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" + } +} +``` + +### Tilde Version Ranges A range specifier starting with a tilde `~` character is matched against a version in the following fashion. @@ -201,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. @@ -220,45 +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", - "version": "2.0.0-beta.1", - "title": "jQuery.Color()", - "author": { - "name": "John Resig", - "url": "https://github.com/jeresig" - }, - "licenses": [ - { - "type": "MIT", - "url": "https://github.com/jquery/jquery-color/raw/2.0.0-beta.1/MIT-LICENSE.txt" - }, - { - "type": "GPLv2", - "url": "https://github.com/jquery/jquery-color/raw/2.0.0-beta.1/GPL-LICENSE.txt" - } - ], - "dependencies": { - "jquery": ">=1.6" - }, - "description": "The main purpose of this plugin is to animate color properties on elements using jQuery's .animate()", - "keywords": [ - "color", - "animate", - "rgba", - "hsla" - ], - "homepage": "https://github.com/jquery/jquery-color", - "maintainers": [ - { - "name": "Corey Frang", - "url": "https://github.com/gnarf37" - } - ] + "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" + } } -``` \ No newline at end of file +``` diff --git a/pages/docs/publish.md b/pages/docs/publish.md new file mode 100644 index 0000000..b3b3cb5 --- /dev/null +++ b/pages/docs/publish.md @@ -0,0 +1,87 @@ + + +
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.
+ +Before you can list your plugin on this site, you'll need to choose a name for 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.
- -We encourage you to follow a few simple tips as well:
-Names are registered on a first come, first serve basis. Registering a name happens automatically the first time you publish a release of your plugin. You cannot reserve a name prior to releasing your plugin. Once you've registered a name, you are the sole owner of that name. Nobody else will be able to publish a release using the same name. There is no limit on how many plugins/names a single person may register, but all plugins must be legitimate.
- -While most plugins will only ever have one owner, there are times when the original owner may move on to other projects and wish to transfer ownership to someone else. There is currently no automated process for this, the original owner must contact a jQuery team member, prove ownership and indicate who the new owner should be.
- -In the case of an abandoned plugin where the original owner is no longer active, the jQuery team can choose to change ownership at their discretion. These will indeed be a rare occurrence, likely requiring an event such as _why or Mark Pilgrim's infosuicide.
- -Certain prefixes will also be blacklisted for individual plugins. Large projects which include many plugins in a single repository, such as jQuery UI, are registered as suites. Each suite is required to have a unique prefix and all of their plugin names must use that prefix. As such, no other plugin may use a name with a suite's prefix. Suites must be manually vetted by the jQuery team.
- -Note: In order to allow proper naming of extensions for plugins in a suite, the prefix blacklisting is only one level deep. For example, jQuery UI owns all ui.*
names, but ui.autocomplete.*
is open to the public.
Publishing your plugin on this site is a simple two step process.
- -First, you'll need to create a post-receive hook on GitHub. Just follow the
-step-by-step guide
-for adding a webhook and set the URL to
-http://plugins.jquery.com/postreceive-hook
. Now you're ready to publish
-your plugin.
Once the post-receive hook is setup, 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 new tag is available and the plugins site -will take care of the rest!
- -The name of the tag must be a valid
-semver 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.
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.
diff --git a/test/service.js b/test/service.js index 836f0cc..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, [ @@ -335,6 +340,18 @@ var tests = { // ]); // }, + "bugs - invalid type {}": function( manifest, fn ) { + manifest.bugs = {}; + fn( manifest, manifest.version, [ + "Invalid data type for bugs.url; must be a string." + ]); + }, + + "bugs - { url: \"valid\" }": function( manifest, fn ) { + manifest.bugs = { url: "http://example.com/bugs/" }; + fn( manifest, manifest.version, [] ); + }, + "maintainers - invalid type": function( manifest, fn ) { manifest.maintainers = "John"; fn( manifest, manifest.version, [