Skip to content

Library best practices

James Burke edited this page Jun 6, 2013 · 13 revisions

Systems work better when there are good default conventions. Configuration should be possible, but for the 80% case, the conventions should be used.

volo uses the following conventions. The more you adhere to them, the easier it will be for others to consume your code.

There are two basic types of libraries: libraries that are a single JS file (jquery), and libraries that are a collection of modular scripts (dojo), and they have slightly different guidelines.

  • Name the repo the same name as the library script.
  • Place the library script at the top of the repo.
  • If it exports a browser global, name it the same name as the library script.
  • Use camelCase for names. JavaScript is camelCased.
  • Do not start with an upper-case letter unless your library is a constructor function that creates new instances via new.
  • You do not need to provide a minified version of your library in the repo. Every developer should know how to minify source as part of code deployment. If you do provide a "minified" version of the script in the repository, name it with a .min.js suffix.
  • Do not place any other JS files at the top level of the repo. Just the library JS file, and optionally, the .min.js version of the file.
  • Host the library on GitHub. You do not have to do development there, just provide the library with tags matching the releases. See Minimizing GitHub Use below.

If your library has specific dependencies it needs, specify them in a /*package.json */ comment in the JS file. See the package.json for more details.

If your library is made up of a few scripts that you combine into one script, consider going the module collection route. You can still go the single file route for distribution by create a package.json file that contains a pointer to the built script. See volo.url in package.json.

Example of the ideal single file library structure, assuming the library name is "foo":

  • foo/
    • foo.js
    • docs/
    • tests/
    • README.md

foo.js looks like this:

/*! license here */

//Specify any dependencies here. You can place this JSON structure in
//a package.json file instead if you prefer.
/*package.json
{
    "volo": {
        "dependencies": {
            "jquery": "jquery/jquery"
        }
    }
}
*/

(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD. Register as an anonymous module.
        define(['jquery'], factory);
    } else {
        // Browser globals
        root.foo = factory(root.jQuery);
    }
}(this, function (jQuery) {
    // Just return a value to define the module export.
    // This example returns an object, but the module
    // can return a function as the exported value.
    return {};
}));

For a collection of modules:

  • If it exports a browser global, name it the same name as the repo name.
  • Use camelCase for names. JavaScript is camelCased.
  • Do not start with an upper-case letter unless your library is a constructor function that creates new instances via new.
  • Create a package.json file that has a "main" property that indicates the main module to use. If there is no main module, it is just a collection of modules that can be used on its own, then do not specify a "main" property in the package.json.
  • If there is a main module that serves as the entry point into the collection of modules, name it main.js and place it at the top level of the repo.
  • Any modules that are to be used by others (public modules), should be placed at the top level of the repo. Any private modules can be in a lib directory that is in the top level.

If your library has specific dependencies it needs, specify them in the package.json file. See the package.json for more details.

If the collection needs to be built in some way before they are distributed, use the volo.url property in the package.json to indicate where to find the built sources.

A collection of modules that form a library called "bar". bar itself is a function, and it has some utility functions off of it that are provided by modules that are only for internal consumption by "bar". This project is constructed as AMD and/or CommonJS/Node modules.

  • bar/
    • main.js
    • package.json
    • lib/
      • zap.js
      • zip.js
    • docs/
    • tests/
    • README.md

main.js looks like this:

/*! license here */

//An AMD module.
define(function (require) {
    var $ = require('jquery');

    function bar() {
    }

    bar.zip = require('./lib/zip');
    bar.zap = require('./lib/zap');

    return bar;
});

The package.json looks like this:

{
    "main": "main",
    "volo": {
        "dependencies": {
            "jquery": "jquery/jquery"
        }
    }
}

When volo installs "bar" it will install it like so:

* js/
    * bar/
    * bar.js

Where bar.js is an adapter module that just returns the "main" module:

//bar.js
define(['bar/main'], function (main) {
    return main;
});

In this way, the project using bar can just require('bar') and get the correct module exports.

A collection of modules that form a library called "bar". bar itself is a function, and it has some utility functions off of it that are provided by modules that are only for internal consumption by "bar".

This project is constructed as AMD and/or CommonJS/Node modules. You could use this kind of layout, but output a built, single JS file. In that case, the layout would be similar, but you would specify a volo.url in the package.json.

  • bar/
    • array.js
    • fn.js
    • object.js
    • package.json
    • docs/
    • tests/
    • README.md

where object.js may look something like this:

/*! license here */

//An AMD module.
define(function (require) {
    var $ = require('jquery');

    function object() {
    }

    object.mixin = function () {};

    return object;
});

The package.json looks like this:

{
    "volo": {
        "dependencies": {
            "jquery": "jquery/jquery"
        }
    }
}

When volo installs "baz" it will install it like so:

  • js/
    • baz/
      • array.js
      • fn.js
      • object.js

The project using baz can get the modules by doing these kinds of calls:

  • require('baz/array')
  • require('baz/fn')
  • require('baz/object')

package.json is a way to specify metadata about a library. It is JSON.

If you are making a single file JS library, you can put the package.json contents in a /*package.json */ comment. It should look like this:

/*package.json
{
    "volo": {}
}
*/

If you are delivering a collection of modules, or prefer to not use a comment in the JS file, use a package.json file that is the top level of the repository. It should just be the JSON structure:

{
    "volo": {}
}

volo understands the following properties in a package.json:

{
    "amd": {},
    "volo": {
        "baseUrl": "",
        "url": "",
        "dependencies": {},
        "type": "",
        "ignore": []
    }
}

amd

If this object exists, then it informs volo that the project is an AMD project, and it should try to convert non-AMD JS dependencies that are added vi volo add with volo amdify. volo add -amdoff can be used to avoid this conversion.

The directory inside this directory to use as the baseUrl to install dependencies. volo will use this directory instead of the directories it would favor by convention.

Only useful for applications, not really for libraries.

Specifies where to find the built version of this library. Either specify and URL to a single JS file, or to a zip file.

You can use {version} in the string value to specify that the version to be installed can be swapped into that location in the URL. This allows you to only set up the URL once in a package.json, no need to change it for every release.

A way to specify external dependencies of this library. The dependency name is specified and the value for the dependency name is one of the following:

  • A GitHub identifier: owner/repo/tag, or owner/repo or repo (which uses github search to convert to owner/repo).
  • An URL to a single JS file or .zip file.

Example:

{
    "volo": {
        "dependencies": {
            "backbone": "documentcloud/backbone/0.9.0",
            "jquery": "jquery",
            "swfobject": "http://swfobject.googlecode.com/files/swfobject_2_2.zip"
        }
    }
}

A way to specify the type of library. Values can be one of the following:

This is useful to use to overrided keeping the directory when otherwise the default add logic would just pull out one JS file.

Introduced in 0.3.0: Specifies an array of top-level files or directories to ignore. If it is specified, then this list is used instead of the default set used for directory installs: [".gitignore", "demo", "demos", "doc", "docs", "example", "examples", "test", "tests"].

volo.ignore supports entries that are supported by node-glob. Example:

{
    "volo": {
        "ignore": [".gitignore", "test", "examples", "**/*.txt"]
    }
}

If you prefer to not use GitHub to develop your code, there are few options that can still be used by default with volo.

Let everyone know where to find a .zip file of your dependency. It can be added to volo via:

volo add http://your.domain.com/path/to/your/lib.zip

This approach does not make it easy to find the .zip file via volo search or when volo add query is used.

Let everyone know the URL to your library file. It can be added to a project via this volo command:

volo add http://your.domain.com/path/to/your/lib.js

This approach does not make it easy to find the .zip file via volo search or when volo add query is used.

Only place a package.json file in GitHub. Set volo.url to indicate where to find the file. Example:

{
    "volo": {
        "url": "http://your.domain.com/path/to/1.2.3/lib.js"
    }
}

Update the the package.json whenever you have a release.

It is also helpful to put in a README.md file that describes where your source is hosted and how to interact with your project.

This approach makes it easy to find the library via volo search or volo add query. However it only means broadcasting your latest release.

Same as the package.json in GitHub approach, but use the special {version} token in the volo.url value:

{
    "volo": {
        "url": "http://your.domain.com/path/to/{version}/lib.js"
    }
}

Then, set a GitHub tag for each release.

This approach makes it easy to find the library via volo search or volo add query, and it allows a developer a way to access specific versions of your library.