RequireJS and jQuery - A Journey in noConflict Mode

Being purveyors of 3rd-Party JavaScript, sandboxing is something near and dear to our hearts. We take integrating with our publishers seriously, aiming to ensure a high-quality experience for their engineers and most importantly their users. jQuery has noConflict() and you’re gold; easy enough, right? In some cases, this isn’t enough and when it isn’t, it can be tricky to understand why.

A Brief Review of jQuery’s noConflict Mode

Our JavaScript should not disrupt anything in use by the publisher, freeing them to focus on delighting their users and not on working with our JavaScript. We rely on jQuery’s noConflict() to isolate our version of jQuery from the version our publishers include on their pages.

Let’s have a look at the relevant portion of the jQuery source, original comments modified to provide context to this scenario.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// jQuery's build system, safe to ignore for the purpose of this example
define([
  "../core",
  "../var/strundefined"
], function(jQuery, strundefined) {

  // The value of the jQuery parameter is Sharethrough's jQuery, e.g. 1.9.

  // Remember the publisher's jQuery, e.g. 1.10, so that we can restore it
  // if noConflict is called.
  var
    _jQuery = window.jQuery,
    _$ = window.$;

  // The job of noConflict is to restore both the $ and jQuery globals to
  // their original values.
  jQuery.noConflict = function(deep) {

    // Repoint the global to the publisher's jQuery (1.10)
    if (window.$ === jQuery) {
      window.$ = _$;
    } 

    // Repoint the global to the publisher's jQuery (1.10)
    if (deep && window.jQuery === jQuery) {
      window.jQuery = _jQuery;
    }

    // Return Sharethrough's jQuery (1.9) so that we can refer to it in our sandbox
    return jQuery;
  });
});

…enabling us to store off our own version of jQuery for our internal use while leaving the globals available to the publisher.

1
STR.Vendor.$ = $.noConflict(true)

The Problem

When our JavaScript was included in a particular publisher’s page, there were errors reported against missing methods on some instance of jQuery. We traced them down to calls like this:

1
2
> $.fn.jquery.browser.msie
TypeError: Cannot read property 'msie' of undefined

In our experience this means our version of jQuery, even though it’s sandboxed, is being queried instead of the publisher’s version. How is this possible? We’re using noConflict() and the sniff test looks good (assuming we’re using different versions of jQuery that is):

1
2
3
4
$.fn.jquery
"1.10.2"
STR.Vendor.$.fn.jquery
"1.9.0"

So then why all the errors?

jQuery Is Mostly No-Conflict - Enter RequireJS

From the title of this post we’re going to assume some basic familiarity with RequireJS. If not, it’s a “JavaScript file and module loader” that’s pretty darn interesting; definitely check it out if you haven’t. For the purpose of this post, I’m going to jump right in.

This is one way in which a module is defined in RequireJS.

1
2
3
4
5
6
7
8
//Explicitly defines the "foo/title" module:
define(
  "foo/title",
  ["my/cart", "my/inventory"],
  function(cart, inventory) {
    //Define foo/title object in here.
  }
);

…and this is present inside of jQuery:

1
2
3
4
5
6
7
8
if (typeof define === "function" && define.amd) {
  define(
    "jquery",
    [],
    function() {
    return jQuery;
  });
}

Following along in our example, this unconditionally defines a “jquery” module using Sharethrough’s version of jQuery. Let’s all let that sink in for a second :)

If Sharethrough’s JavaScript is included before the publisher’s jQuery is pulled in, that means the publisher has a ‘jquery’ RequireJS module defined that points to Sharethrough’s jQuery. As of RequireJS 2.2, a module cannot be redefined hence when this module is referred to, it will always be Sharethrough’s jQuery.

Yikes, What Now?!

Since this behaviour cannot be controlled in jQuery, that means we need to do something after the fact. We thought about filing a bug and submitting a pull request as we usually do alas, eight months ago - Ticket #13928 (closed bug: notabug).

We decided to blanket undef and redefine the module if RequireJS is present. Since we use noConflict mode, this is guaranteed to reference the proper version of jQuery defined on the page.

1
2
3
4
5
6
STR.amdNoConflict = ->
  if typeof define is "function" and define.amd
    if requirejs
      requirejs.undef 'jquery'
      define "jquery", [], ->
        window.jQuery

Anyone out there run into something similar? If so, how did you solve it?