npm Blog (Archive)

The npm blog has been discontinued.

Updates from the npm team are now published on the GitHub Blog and the GitHub Changelog.

Making your jQuery plugin work better with npm tools

In the last article, we were developing an app that used the tipso jQuery plugin. We were using a plugin that we didn’t author. But what if we were the author of the tipso plugin? What could we do to make it easier to use tipso with Browserify, or to use with other modules in the npm ecosystem?

As we saw in the last article, jQuery plugins can be used as is with tools like Browserify. However, there are two improvements you can make so that it’s easier to do:

  1. Provide paths to your assets so that consumers of your plugin don’t have to hardcode paths in package.json
  2. Make it possible to use your plugin either as a browser global or as a CommonJS module so that when it is loaded in Node.js contexts, it can be handled like any other module

Of these two improvements, the first provides much more benefit to your consumers than the second, but you can also do both :)

Providing paths to assets in package.json

In the last article, the app developer needed to add the following code to their package.json:

"browser": {
  "tipso": "./node_modules/tipso/src/tipso.js"
},
"style": [
  "./node_modules/tipso/src/tipso.css"
]

If this information hadn’t been in the app’s package.json, Browserify and parcelify would not have known where to find the files. However, hardcoding these paths causes a problem. If the tipso publisher decides to move the files—for example, to rename src to lib—then any application that has this path hardcoded in their package.json file will break until they figure out that the change has been made and update their paths.

This information—where the files in your package live—should be maintained in the package itself, not by the app depending on it. As a publisher, you can add this information to your package.json file.

JavaScript file path

You can provide the path to your .js file using the main field.

"main": "./src/tipso.js"

Browserify will look at this main field to figure out which .js file it should include in the bundle. It should be the path to your main .js file as a string. If you have multiple .js files, you should either concatenate them into a single file, or set them up as modules (which we’ll explain below) and require them.

Note: In the latest version of tipso, the main field has already been added.

CSS file path

If you want to support parcelify for CSS processing, then you can add the style field.

"style": [
  "./src/tipso.css"
]

In contrast to the main field, the style field can take multiple file paths, so you can add multiple CSS files. You can also indicate that any files that match a certain pattern (using glob patterns) should be included. For example, you could say that any CSS that’s in the root directory should be added.

"style": "*.css"

Note that while the main field is very widely used, the style field is still an emerging convention so you might not see it in some packages.

Making your package compatible with CommonJS

As we’ve shown up to this point, you don’t need to add CommonJS support to your plugin for it to work with Browserify. However, it’s pretty easy to add support if you want to.

You could use the Universal Module Definition… and you can automate the process using grunt or gulp tasks. Browserify even makes it possible to generate a build which supports these different module formats with the --standalone flag.

In this article, we’ll show you how to simply add CommonJS module support, but you can check out those more complete options, too.

What will we be changing?

In the original plugin, we had a function which was immediately called when the browser hit it (often refered to as an IIFE). When it was called, the global jQuery object (and a couple of other globals) were passed in.

(function($, window, document, undefined) {
  // add .tipso() to the jQuery object passed in.
})(jQuery, window, document);

What we need to do is change this to first detect what kind of environment we’re working in. If we’re in the browser, we can continue to pass in the global jQuery object. However, if we’re working with a tool like Browserify, we should use require to get the jQuery object so the application can decide whether it wants to make jQuery available globally or not.

Detecting the environment

To detect this, we’ll check to see whether our environment supports module.exports, which both Node.js and Browserify do, but the browser does not.

if (typeof module === "object" && typeof module.exports === "object") {
  // create the tipso plugin with a CommonJS version of jQuery
} else {
  // create the tipso plugin with the global jQuery object
}

Adding the plugin to the jQuery object

So now that we’ve detected our environment, we want to use that knowledge to wire up the tipso plugin. This is pretty easy. We simply take the function that we had before, which was immediately invoked as an IIFE, and call that a factory. Based on the environment, we either pass the factory the jQuery object we get from the module system using require, or we pass it the global jQuery object.

if(typeof module === "object" && typeof module.exports === "object") {
  factory(require("jquery"), window, document);
} else {
  factory(jQuery, window, document);
}

This is generally wrapped up in its own IIFE:

(function (factory) {
  if(typeof module === "object" && typeof module.exports === "object") {
    factory(require("jquery"), window, document);
  } else {
    factory(jQuery, window, document);
  }
}(function($, window, document, undefined) {
  // code for attaching tipso to whatever jQuery was passed in goes here.
}));

If this code is confusing, try the annotated version.

Note: because you are using `require` to pull in jQuery here, you’ll need to add it as a dependency in `package.json`:

"dependencies": {
  "jquery": "^2.1.3"
}

Exporting the plugin as a module (optional)

The above step is all that needs to happen for this to work. You’ve attached tipso to the jQuery object, whether that object is global or handled by a module system and imported with require. Your consumers will be looking for tipso on that jQuery object, so it works for them as is.

However, it’s generally expected that an object will be returned from require, and some consumers might want to have a direct reference to the plugin. To do this, you can use module.exports, so the code for creating the CommonJS version of tipso becomes:

if(typeof module === "object" && typeof module.exports === "object") {
  module.exports = factory(require("jquery"), window, document);
}

You need to make sure that the factory function returns something, too.

As stated above, your consumers aren’t likely to use the object exported from this anyway, so this isn’t a requirement.

What has this change done?

This change has made it possible to get all of the functionality of jQuery and jQuery plugins without exposing jQuery as a global.

In the example from the last article, you can now change the line:

global.jQuery = require('jquery')
to
var jQuery = require('jquery')

Make sure that you run npm dedupe before trying this out to remove any extra copies of jQuery. You don’t want to have multiple copies of jQuery in your dependency tree because that can cause hard-to-debug issues.

When you use Browserify to bundle this up again and open the page in the browser, you’ll find that even though the jQuery code has run and the tipso plugin has done its magic, jQuery isn’t defined in the global scope. You can confirm this by opening up the browser console and entering console.log(jQuery).