From c81ace882e11af4902caec2f475aab1b5904a636 Mon Sep 17 00:00:00 2001 From: Jeff Williams Date: Mon, 20 Oct 2014 09:34:54 -0700 Subject: [PATCH] extract more information from package.json files; add tests (#710) --- lib/jsdoc/package.js | 252 ++++++++++++++++++++++++++++------- test/specs/jsdoc/package.js | 259 ++++++++++++++++++++++++++++++++++++ 2 files changed, 464 insertions(+), 47 deletions(-) create mode 100644 test/specs/jsdoc/package.js diff --git a/lib/jsdoc/package.js b/lib/jsdoc/package.js index 135c9abc1..b390adc9e 100644 --- a/lib/jsdoc/package.js +++ b/lib/jsdoc/package.js @@ -1,70 +1,228 @@ +'use strict'; + +var logger = require('jsdoc/util/logger'); + /** - @overview - @author Michael Mathews - @license Apache License 2.0 - See file 'LICENSE.md' in this project. + * Provides access to information about a JavaScript package. + * + * @module jsdoc/package + * @see https://www.npmjs.org/doc/files/package.json.html + */ + +// Collect all of the license information from a `package.json` file. +function getLicenses(packageInfo) { + var licenses = packageInfo.licenses ? packageInfo.licenses.slice(0) : []; + + if (packageInfo.license) { + licenses.push({ type: packageInfo.license }); + } + + if (licenses.length) { + return licenses; + } +} + +/** + * Information about where to report bugs in the package. + * + * @typedef {Object} module:jsdoc/package.Package~BugInfo + * @property {string} email - The email address for reporting bugs. + * @property {string} url - The URL for reporting bugs. + */ + +/** + * Information about a package's software license. + * + * @typedef {Object} module:jsdoc/package.Package~LicenseInfo + * @property {string} type - An identifier for the type of license. + * @property {string} url - The URL for the complete text of the license. + */ + +/** + * Information about a package author or contributor. + * + * @typedef {Object} module:jsdoc/package.Package~PersonInfo + * @property {string} name - The person's full name. + * @property {string} email - The person's email address. + * @property {string} url - The URL of the person's website. */ -'use strict'; /** - @module jsdoc/package - @see http://wiki.commonjs.org/wiki/Packages/1.0 + * Information about a package's version-control repository. + * + * @typedef {Object} module:jsdoc/package.Package~RepositoryInfo + * @property {string} type - The type of version-control system that the repository uses (for + * example, `git` or `svn`). + * @property {string} url - The URL for the repository. */ /** - @class - @classdesc Represents a JavaScript package. - @param {string} json - The contents of package.json. + * Information about a JavaScript package. JSDoc can extract package information from + * `package.json` files that follow the + * [npm specification](https://www.npmjs.org/doc/files/package.json.html). + * + * **Note**: JSDoc does not validate or normalize the contents of `package.json` files. If your + * `package.json` file does not follow the npm specification, some properties of the `Package` + * object may not use the format documented here. + * + * @class + * @param {string} json - The contents of the `package.json` file. */ exports.Package = function(json) { - json = json || '{}'; + var packageInfo; - /** The source files associated with this package. - @type {Array} + /** + * The string identifier that is shared by all `Package` objects. + * + * @readonly + * @default + * @type {string} */ - this.files = []; - - /** The kind of this package. - @readonly - @default - @type {string} - */ this.kind = 'package'; - json = JSON.parse(json); + try { + packageInfo = JSON.parse(json || '{}'); + } + catch (e) { + logger.error('Unable to parse the package file: %s', e.message); + packageInfo = {}; + } - /** The name of this package. - This value is found in the package.json file passed in as a command line option. - @type {string} - */ - this.name = json.name; + /** + * The package name. + * + * @type {string} + */ + this.name = packageInfo.name; - /** The longname of this package. - @type {string} - */ + /** + * The unique longname for this `Package` object. + * + * @type {string} + */ this.longname = this.kind + ':' + this.name; - /** The description of this package. - @type {string} - */ - this.description = json.description; + /** + * The author of this package. Contains either a + * {@link module:jsdoc/package.Package~PersonInfo PersonInfo} object or a string with + * information about the author. + * + * @type {(module:jsdoc/package.Package~PersonInfo|string)} + * @since 3.3.0 + */ + this.author = packageInfo.author; + + /** + * Information about where to report bugs in the project. May contain a URL, as a string, or + * an object with more detailed information. + * + * @type {(string|module:jsdoc/package.Package~BugInfo)} + * @since 3.3.0 + */ + this.bugs = packageInfo.bugs; + + /** + * The contributors to this package. + * + * @type {Array.<(module:jsdoc/package.Package~PersonInfo|string)>} + * @since 3.3.0 + */ + this.contributors = packageInfo.contributors; + + /** + * The dependencies for this package. + * + * @type {Object} + * @since 3.3.0 + */ + this.dependencies = packageInfo.dependencies; + + /** + * A brief description of the package. + * + * @type {string} + */ + this.description = packageInfo.description; + + /** + * The development dependencies for this package. + * + * @type {Object} + * @since 3.3.0 + */ + this.devDependencies = packageInfo.devDependencies; + + /** + * The JavaScript engines that this package supports. Each key is a string that identifies the + * engine (for example, `node`). Each value is a + * [semver](https://www.npmjs.org/doc/misc/semver.html)-compliant version number for the engine. + * + * @type {Object} + * @since 3.3.0 + */ + this.engines = packageInfo.engines; + + /** + * The source files associated with the package. + * + * New `Package` objects always contain an empty array, regardless of whether the `package.json` + * file includes a `files` property. + * + * After JSDoc parses your input files, it sets this property to a list of paths to your input + * files. + * + * @type {Array.} + */ + this.files = []; + + /** + * The URL for the package's homepage. + * + * @type {string} + * @since 3.3.0 + */ + this.homepage = packageInfo.homepage; + + /** + * Keywords to help users find the package. + * + * @type {Array.} + * @since 3.3.0 + */ + this.keywords = packageInfo.keywords; /** - The hash summary of the source file. - @type {string} - @since 3.2.0 - */ - this.version = json.version; + * The licenses used by this package. Combines information from the `package.json` file's + * `license` property and the deprecated `licenses` property. + * + * @type {Array.} + */ + this.licenses = getLicenses(packageInfo); + + /** + * The module ID that provides the primary entry point to the package. For example, if your + * package is a CommonJS module, and the value of this property is `foo`, users should be able + * to load your module with `require('foo')`. + * + * @type {string} + * @since 3.3.0 + */ + this.main = packageInfo.main; + + /** + * The version-control repository for the package. + * + * @type {module:jsdoc/package.Package~RepositoryInfo} + * @since 3.3.0 + */ + this.repository = packageInfo.repository; /** - * The licenses of this package. - * @type {Array} - * @example - * "licenses": [ - * { - * "type": "GPLv2", - * "url": "http://www.example.com/licenses/gpl.html" - * } - * ] + * The [semver](https://www.npmjs.org/doc/misc/semver.html)-compliant version number of the + * package. + * + * @type {string} + * @since 3.2.0 */ - this.licenses = json.licenses; + this.version = packageInfo.version; }; diff --git a/test/specs/jsdoc/package.js b/test/specs/jsdoc/package.js new file mode 100644 index 000000000..80ca8a4e7 --- /dev/null +++ b/test/specs/jsdoc/package.js @@ -0,0 +1,259 @@ +/*global beforeEach, describe, expect, it, spyOn */ +'use strict'; + +describe('jsdoc/package', function() { + var emptyPackage; + var jsdocPackage = require('jsdoc/package'); + var logger = require('jsdoc/util/logger'); + var Package = jsdocPackage.Package; + + function checkPackageProperty(name, value) { + var myPackage; + var obj = {}; + + obj[name] = value; + myPackage = new Package( JSON.stringify(obj) ); + + // use toEqual so we can test array/object values + expect(myPackage[name]).toEqual(value); + } + + it('should exist', function() { + expect(jsdocPackage).toBeDefined(); + expect(typeof jsdocPackage).toBe('object'); + }); + + it('should export a "Package" constructor', function() { + expect(Package).toBeDefined(); + expect(typeof Package).toBe('function'); + }); + + describe('Package', function() { + beforeEach(function() { + emptyPackage = new Package(); + }); + + it('should accept a JSON-format string', function() { + function newPackage() { + return new Package('{"foo": "bar"}'); + } + + expect(newPackage).not.toThrow(); + }); + + it('should work when called with no arguments', function() { + function newPackage() { + return new Package(); + } + + expect(newPackage).not.toThrow(); + }); + + it('should log an error when called with bad input', function() { + function newPackage() { + return new Package('abcdefg'); + } + + spyOn(logger, 'error'); + + expect(newPackage).not.toThrow(); + expect(logger.error).toHaveBeenCalled(); + }); + + describe('author', function() { + it('should be undefined by default', function() { + expect(emptyPackage.author).not.toBeDefined(); + }); + + it('should contain the value from the package file', function() { + checkPackageProperty('author', { name: 'Jane Smith', email: 'jsmith@example.com' }); + }); + }); + + describe('bugs', function() { + it('should be undefined by default', function() { + expect(emptyPackage.bugs).not.toBeDefined(); + }); + + it('should contain the value from the package file', function() { + checkPackageProperty('bugs', { url: 'http://example.com/bugs' }); + }); + }); + + describe('contributors', function() { + it('should be undefined by default', function() { + expect(emptyPackage.contributors).not.toBeDefined(); + }); + + it('should contain the value from the package file', function() { + checkPackageProperty('contributors', [{ + name: 'Jane Smith', + email: 'jsmith@example.com' + }]); + }); + }); + + describe('dependencies', function() { + it('should be undefined by default', function() { + expect(emptyPackage.dependencies).not.toBeDefined(); + }); + + it('should contain the value from the package file', function() { + checkPackageProperty('dependencies', { bar: '~1.1.0' }); + }); + }); + + describe('description', function() { + it('should be undefined by default', function() { + expect(emptyPackage.description).not.toBeDefined(); + }); + + it('should contain the value from the package file', function() { + checkPackageProperty('description', 'My package.'); + }); + }); + + describe('devDependencies', function() { + it('should be undefined by default', function() { + expect(emptyPackage.devDependencies).not.toBeDefined(); + }); + + it('should contain the value from the package file', function() { + checkPackageProperty('devDependencies', { baz: '~3.4.5' }); + }); + }); + + describe('engines', function() { + it('should be undefined by default', function() { + expect(emptyPackage.engines).not.toBeDefined(); + }); + + it('should contain the value from the package file', function() { + checkPackageProperty('engines', { node: '>=0.10.3' }); + }); + }); + + describe('files', function() { + it('should contain an empty array by default', function() { + expect(emptyPackage.files).toBeDefined(); + expect(emptyPackage.files).toEqual([]); + }); + + it('should ignore the value from the package file', function() { + var myPackage = new Package('{"files": ["foo", "bar"]}'); + + expect(myPackage.files.length).toBe(0); + }); + }); + + describe('homepage', function() { + it('should be undefined by default', function() { + expect(emptyPackage.homepage).not.toBeDefined(); + }); + + it('should contain the value from the package file', function() { + checkPackageProperty('homepage', 'http://example.com/'); + }); + }); + + describe('keywords', function() { + it('should be undefined by default', function() { + expect(emptyPackage.keywords).not.toBeDefined(); + }); + + it('should contain the value from the package file', function() { + checkPackageProperty('keywords', ['foo', 'bar']); + }); + }); + + describe('licenses', function() { + it('should be undefined by default', function() { + expect(emptyPackage.licenses).not.toBeDefined(); + }); + + it('should contain the value from the package file', function() { + checkPackageProperty('licenses', [{ + type: 'My Open-Source License', + url: 'http://example.com/oss' + }]); + }); + + it('should contain the value of "license" from the package file', function() { + var myPackage = new Package('{"license": "My-OSS-License"}'); + + expect(myPackage.license).not.toBeDefined(); + expect(myPackage.licenses).toBeDefined(); + expect(myPackage.licenses.length).toBe(1); + expect(myPackage.licenses[0].type).toBe('My-OSS-License'); + }); + + it('should combine the "license" and "licenses" properties', function() { + var packageInfo = { + license: 'My-OSS-License', + licenses: [{ + type: 'My Open-Source License', + url: 'http://example.com/oss' + }] + }; + var myPackage = new Package( JSON.stringify(packageInfo) ); + + expect(myPackage.licenses.length).toBe(2); + }); + }); + + describe('longname', function() { + it('should default to "package:undefined"', function() { + expect(emptyPackage.longname).toBe('package:undefined'); + }); + + it('should reflect the value of the "name" property', function() { + var myPackage = new Package('{"name": "foo"}'); + + expect(myPackage.longname).toBe('package:foo'); + }); + }); + + describe('main', function() { + it('should be undefined by default', function() { + expect(emptyPackage.main).not.toBeDefined(); + }); + + it('should contain the value from the package file', function() { + checkPackageProperty('main', 'foo'); + }); + }); + + describe('name', function() { + it('should be undefined by default', function() { + expect(emptyPackage.name).not.toBeDefined(); + }); + + it('should contain the value from the package file', function() { + checkPackageProperty('name', 'foo'); + }); + }); + + describe('repository', function() { + it('should be undefined by default', function() { + expect(emptyPackage.repository).not.toBeDefined(); + }); + + it('should contain the value from the package file', function() { + checkPackageProperty('repository', { + type: 'git', + url: 'git@example.org:foo/bar/baz.git' + }); + }); + }); + + describe('version', function() { + it('should be undefined by default', function() { + expect(emptyPackage.version).not.toBeDefined(); + }); + + it('should contain the value from the package file', function() { + checkPackageProperty('version', '0.1.2'); + }); + }); + }); +});