41

I'm using browserify-shim and I want to use a generic jQuery plugin. I have looked over the Browserify-shim docs multiple times and I just can't seem to understand what's going on and/or how it knows where to put plugins, attach to the jQuery object etc. Here's what my package.json file looks like:

"browser": {
  "jquery": "./src/js/vendor/jquery.js",
  "caret": "./src/js/vendor/jquery.caret.js"
},

"browserify-shim": {
  "caret": {
     "depends": ["jquery:$"]
  }
}

According the the example given on the browserify-shim documentation, I don't want to specify an exports because this plugin (and most if not all jQuery plugins) attach themselves to the jQuery object. Unless I'm doing something wrong above, I don't understand why it doesn't work (I get an error telling me the function is undefined) when I use it. See below:

$('#contenteditable').caret(5);  // Uncaught TypeError: undefined is not a function

So my question is, how does one configure a generic jQuery plugin (which attaches itself to the jQuery object) with browserify and browserify-shim?

Glen Selle
  • 3,966
  • 4
  • 37
  • 59

4 Answers4

98

After revisiting this and trying some more things, I finally wrapped my head around what browserify-shim is doing and how to use it. For me, there was one key principle I had to grasp before I finally understood how to use browserify-shim. There are basically two ways to use browserify-shim for two different use cases: exposing & shimming.

Background

Let's say you want to just drop in a script tag in your markup (for testing or performance reasons like caching, CDN & the like). By including a script tag in the markup the browser will hit the script, run it, and most likely attach a property on the window object (also known as a global in JS). Of course this can be accessed by either doing myGlobal or window.myGlobal. But there's an issue with either syntax. It doesn't follow the CommonJS spec which means that if a module begins supporting CommonJS syntax (require()), you're not able to take advantage of it.

The Solution

Browserify-shim allows you to specify a global you'd like "exposed" through CommonJS require() syntax. Remember, you could do var whatever = global; or var whatever = window.global; but you could NOT do var whatever = require('global') and expect it to give you the right lib/module. Don't be confused about the name of the variable. It could be anything arbitrary. You're essentially making a global variable a local variable. It sounds stupid, but its the sad state of JS in the browser. Again, the hope is that once a lib supports CommonJS syntax it will never attach itself via a global on the window object. Which means you MUST use require() syntax and assign it to a local variable and then use it wherever you need it.

Note: I found variable naming slightly confusing in the browserify-shim docs/examples. Remember, the key is that you want to include a lib as if it were a properly behaving CommonJS module. So what you end up doing is telling browserify that when you require myGlobal require('myGlobal') you actually just want to be given the global property on the window object which is window.myGlobal.

In fact, if you're curious as to what the require function actually does, it's pretty simple. Here's what happens under the hood:

var whatever = require('mygGlobal');

becomes...

var whatever = window.mygGlobal;

Exposing

So with that background, let's see how we expose a module/lib in our browserify-shim config. Basically, you tell browserify-shim two things. The name you want it accessible with when you call require() and the global it should expect to find on the window object. So here's where that global:* syntax comes in. Let's look at an example. I want to drop in jquery as a script tag in index.html so I get better performance. Here's what I'd need to do in my config (this would be in package.json or an external config JS file):

"browserify-shim": {
  "jquery": "global:$"
}

So here's what that means. I've included jQuery somewhere else (remember, browserify-shim has no idea where we put our tag, but it doesn't need to know), but all I want is to be given the $ property on the window object when I require the module with the string parameter "jquery". To further illustrate. I could also have done this:

"browserify-shim": {
  "thingy": "global:$"
}

In this case, I'd have to pass "thingy" as the parameter to the require function in order to get an instance of the jQuery object back (which it's just getting jQuery from window.$):

var $ = require('thingy');

And yes, again, the variable name could be anything. There's nothing special about $ being the same as the global property $ the actual jQuery library uses. Though it makes sense to use the same name to avoid confusion. This ends up referencing the the $ property on the window object, as selected by the global:$ value in the browserify-shim object in package.json.

Shimming

Ok, so that pretty much covers exposing. The other main feature of browserify-shim is shimming. So what's that? Shimming does essentially the same thing as exposing except rather than including the lib or module in HTML markup with something like a script tag, you tell browserify-shim where to grab the JS file locally. There's no need to use the global:* syntax. So let's refer back to our jQuery example, but this time suppose we are not loading jQuery from a CDN, but simply bundling it with all the JS files. So here's what the config would look like:

"browser": {
  "jquery": "./src/js/vendor/jquery.js", // Path to the local JS file relative to package.json or an external shim JS file
},
"browserify-shim": {
  "jquery": "$"
},

This config tells browserify-shim to load jQuery from the specified local path and then grab the $ property from the window object and return that when you require jQuery with a string parameter to the require function of "jquery". Again, for illustrative purposes, you can also rename this to anything else.

"browser": {
  "thingy": "./src/js/vendor/jquery.js", // Path to the local JS file relative to package.json or an external shim JS file
},
"browserify-shim": {
  "thingy": "$"
},

Which could be required with:

var whatever = require('thingy');

I'd recommend checking out the browserify-shim docs for more info on the long-hand syntax using the exports property and also the depends property which allows you to tell browserify-shim if a lib depends on another lib/module. What I've explained here applies to both.

Anonymous Shimming

Anonymous shimming is an alternative to browserify-shim which lets you transform libs like jQuery into UMD modules using browserify's --standalone option.

$ browserify ./src/js/vendor/jquery.js -s thingy > ../dist/jquery-UMD.js

If you dropped that into a script tag, this module would add jQuery onto the window object as thingy. Of course it could also be $ or whatever you like.

If however, it's requireed into your browserify'd app bundle, var $ = require("./dist/jquery-UMD.js");, you will have jQuery available inside the app without adding it to the window object.

This method doesn't require browserify-shim and exploits jQuery's CommonJS awareness where it looks for a module object and passes a noGlobal flag into its factory which tells it not to attach itself to the window object.

starball
  • 20,030
  • 7
  • 43
  • 238
Glen Selle
  • 3,966
  • 4
  • 37
  • 59
  • Everywhere it talks about "in the package.json" file *which* package.json file is this? the main project package.json file that has bowserify defined in it?? or within a subfolder or what? I feel like there is a piece of information I'm missing so it's not connecting for me. – Misterparker Sep 08 '14 at 23:36
  • 1
    No, you're right. It's just the package.json file in the root of the project (where you have all your NPM dependencies defined) – Glen Selle Sep 08 '14 at 23:39
  • Another note I would add that is not immediately obvious: the library you are exposing determines what your `browserify-shim: { 'exposeThis': 'exposedByLibrary' }` configuration must set. For example, Zepto exposes `window.$` and `window.Zepto`, so your configuration must read either `browserify-shim: { 'exposeThis': '$' }` or `browserify-shim: { 'exposeThis': 'Zepto' }`, otherwise your require('exposeThis') will return undefined. – Clev3r Sep 19 '14 at 14:26
  • 1
    Thanks for the info :) Although you didn't actually talk about plugins in your answer... :/ – Jamie Hutber Sep 23 '14 at 09:22
  • @JamieHutber True, but I'm confident once a person can understand how something works, they can intuitively do it in their specific project. Also, the way in which you include a jQuery plugin will differ on a project-by-project basis since every plugin is different & the way in which you include it could change. – Glen Selle Oct 10 '14 at 17:06
  • 25
    Thanks for the entire explanation but how did you go about integrating that jquery plugin after all? – David Oct 10 '14 at 19:02
  • Great great explanation, much clearer for me than the doc! Thanks a lot for that! – xav Oct 21 '14 at 16:53
  • 1
    Thanks for the explanation. You should give back to the documentation. This is better than the official doc. – Evan Carroll Dec 02 '14 at 22:00
  • 1
    Brilliant explanation, thank you for this Glen! Been going round in circles trying to get shimming to work and wondering why my result was always undefined. This made it all clear :) – Michael Martin Dec 04 '14 at 22:05
  • I'm glad my answer was helpful to quite a few people though I think it's worth pointing out the API has changed in the latest version. So unless you're using the older version, be aware the APIs might be different but the concepts are mostly the same. – Glen Selle Jan 12 '15 at 03:18
  • 1
    This is a wonderful answer that details browserify-shim better than the docs, but even with this explanation I cannot get a jquery plugin to work. You really should not assume that your answer gives enough information to help the user understand what to do next. – jamis Jan 13 '15 at 07:55
  • 1
    Whats the answer here? Can we see a finished example? Im still getting the undefined is not a function error – jennas Aug 05 '15 at 05:41
  • Thanks for the explanation but is there any chance you would be able to show the complete example? – E.H. Jan 07 '16 at 23:44
  • Very helpful post. @Clever is correct though: the `$` symbol, for example, is certainly not arbitrary as you wrongly state. Because browserify needs to look up the symbol on `window`. But, if you're using the `global:`, qualifier, or the browser tag, then you are correct about `jquery` <=> `thingy`. – Cool Blue Jul 10 '16 at 12:51
  • @CoolBlue Care to edit the answer so it's "correct"-- I fail to see where I ever stated `$` was arbitrary. There are two contexts I believe you're confusing. Sure, you have to explicitly state the global that a library exposes--this is not arbitrary (I never said it was), but what you alias that exposed global as, is *completely* arbitrary--a point I reiterated several times in the post. – Glen Selle Jul 10 '16 at 20:05
  • @GlenSelle sorry about that! Of course you are correct: I got myself confused. Anyway, I have offered an edit that maybe makes it more obvious, since it is an extremely confusing concept (at least for me it is). I also offered an additional approach that shims anonymously, as long as your lib is CommonJS aware (as jQuery is these days). Please feel free to roll it back if you don't think it fits with your original content! – Cool Blue Jul 11 '16 at 16:34
  • @CoolBlue Looks fine. Thanks! – Glen Selle Jul 11 '16 at 16:57
  • @GlenSelle Hi, Your explanation is great. Can we shim a library directly via CDN url without linking it in HTML code? In my case, I want a reference to `google` variable. But when my test runs the HTML page is not loaded. – Rohith K P Jun 21 '17 at 18:45
13

For everyone, who is looking for a concrete example:

The following is an example of package.json and app.js files for a jQuery plugin that attaches itself to the jQuery/$ object, e.g.: $('div').expose(). I don't want jQuery to be a global variable (window.jQuery) when I require it, that's why jQuery is set to 'exports': null. However, because the plugin is expecting a global jQuery object to which it can attach itself, you have to specify it in the dependency after the filename: ./jquery-2.1.3.js:jQuery. Furthermore you need to actually export the jQuery global when using the plugin, even if you don't want to, because the plugin won't work otherwise (at least this particular one).

package.json

{
  "name": "test",
  "version": "0.1.0",
  "description": "test",
  "browserify-shim": {
    "./jquery-2.1.3.js": { "exports": null },
    "./jquery.expose.js": { "exports": "jQuery", "depends": [ "./jquery-2.1.3.js:jQuery" ] }
  },
  "browserify": {
    "transform": [
      "browserify-shim"
    ]
  }
}

app.js

// copy and delete any previously defined jQuery objects
if (window.jQuery) {
  window.original_jQuery = window.jQuery;
  delete window.jQuery;

  if (typeof window.$.fn.jquery === 'string') {
    window.original_$ = window.$;
    delete window.$;
  }
}

// exposes the jQuery global
require('./jquery.expose.js');
// copy it to another variable of my choosing and delete the global one
var my_jQuery = jQuery;
delete window.jQuery;

// re-setting the original jQuery object (if any)
if (window.original_jQuery) { window.jQuery = window.original_jQuery; delete window.original_jQuery; }
if (window.original_$) { window.$ = window.original_$; delete window.original_$; }

my_jQuery(document).ready(function() {
  my_jQuery('button').click(function(){
    my_jQuery(this).expose();
  });
});

In the above example I didn't want my code to set any globals, but I temporarily had to do so, in order to make the plugin work. If you only need jQuery, you could just do this and don't need any workaround: var my_jQuery = require('./jquery-2.1.3.js'). If you are fine with your jQuery being exposed as a global, then you can modify the above package.json example like so:

  "browserify-shim": {
    "./jquery-2.1.3.js": { "exports": "$" },
    "./jquery.expose.js": { "exports": null, "depends": [ "./jquery-2.1.3.js" ] }

Hope that helps some people, who were looking for concrete examples (like I was, when I found this question).

gottlike
  • 296
  • 3
  • 7
  • THANK YOU! I've spend almost a day trying to get my browserify work. – Kesha Antonov Jan 07 '16 at 20:37
  • I'm sure this thread will get closed for the thank yous, but thank you. the whole concept of shimming isn't well explained anywhere and neither it the bloody syntax to make things work. your writeup helped me to understand how to even test the behaviour – ekkis Jul 13 '16 at 03:25
  • Taking it a step further, you can actually nip it in the bud and avoid polluting the `window` object in the first place, but only if you are OK to add a simple header to the plugin. Just for completeness, I added that method in a separate answer. – Cool Blue Jul 17 '16 at 13:47
1

Just for completeness, here is a method that exploits jQuery's CommonJS awareness to avoid having to worry about polluting the window object without actually needing to shim.

Features

  1. jQuery included in the bundle
  2. plugin included in the bundle
  3. no pollution of the window object

Config

In ./package.json, add a browser node to create aliases for the resource locations. This is purely for convenience, there is no need to actually shim anything because there is no communications between the module and the global space (script tags).

{
  "main": "app.cb.js",
  "scripts": {
    "build": "browserify ./app.cb.js > ./app.cb.bundle.js"
  },
  "browser": {
    "jquery": "./node_modules/jquery/dist/jquery.js",
    "expose": "./js/jquery.expose.js",
    "app": "./app.cb.js"
  },
  "author": "cool.blue",
  "license": "MIT",
  "dependencies": {
    "jquery": "^3.1.0"
  },
  "devDependencies": {
    "browserify": "^13.0.1",
    "browserify-shim": "^3.8.12"
  }
}

Method

  • Because jQuery is CommonJS-aware these days, it will sense the presence of the module object provided by browserify and return an instance, without adding it to the window object.
  • In the app, require jquery and add it to the module.exports object (along with any other context that needs to be shared).
  • Add a single line at the start of the plugin to require the app to access the jQuery instance it created.
  • In the app, copy the jQuery instance to $ and use jQuery with the plugin.
  • Browserify the app, with default options, and drop the resulting bundle into a script tag in your HTML.

Code

app.cb.js

var $ = module.exports.jQuery = require("jquery");
require('expose');

$(document).ready(function() {

    $('body').append(
        $('<button name="button" >Click me</button>')
            .css({"position": "relative",
                  "top": "100px", "left": "100px"})
            .click(function() {
                $(this).expose();
            })
    );
});

at the top of the plugin

var jQuery = require("app").jQuery;

in the HTML

<script type="text/javascript" src="app.cb.bundle.js"></script>

Background

The pattern used by jQuery is to call it's factory with a noGlobal flag if it senses a CommonJS environment. It will not add an instance to the window object and will return an instance as always.

The CommonJS context is created by browserify by default. Below is an simplified extract from the bundle showing the jQuery module structure. I removed the code dealing with isomorphic handling of the window object for the sake of clarity.

3: [function(require, module, exports) {

    ( function( global, factory ) {

        "use strict";

        if ( typeof module === "object" && typeof module.exports === "object" ) {
            module.exports = factory( global, true );
        } else {
            factory( global );
        }

    // Pass this if window is not defined yet
    } )( window, function( window, noGlobal ) {

    // ...

    if ( !noGlobal ) {
        window.jQuery = window.$ = jQuery;
    }

    return jQuery;
    }) );
}, {}]

The best method I found is to get things working in the node module system and then it will work every time after browserify-ing.
Just use jsdom to shim the window object so that the code is isomorphic. Then, just focus on getting it to work in node. Then, shim any traffic between the module and global space and finally browserify it and it will just work in the browser.

Cool Blue
  • 6,438
  • 6
  • 29
  • 68
0

I was using wordpress. Hence, I was kind of forced to use the wordpress core's jQuery, available in window object.

It was generating slick() not defined error, when I tried to use slick() plugin from npm. Adding browserify-shim didn't help much.

I did some digging and found out that require('jquery') was not consistent always.

In my theme javascript file, it was calling the wordpress core's jquery.

But, in slick jquery plugin it was calling the latest jquery from node modules.

Finally, I was able to solve it. So, sharing the package.json and gulpfile configuration.

package.json:

"browserify": { "transform": [ "browserify-shim" ] }, "browserify-shim": { "jquery": "global:jQuery" },

gulpfile.babel.js:

browserify({entries: 'main.js', extensions: ['js'], debug: true}) .transform(babelify.configure({ presets: ["es2015"] })) .transform('browserify-shim', {global: true})

Doing transform 'browserify-shim' was crucial part, I was missing earlier. Without it browserify-shim was not consistent.

Jashwant
  • 28,410
  • 16
  • 70
  • 105