Skip to Content

True universal JavaScript modules with write-once-run-anywhere Jasmine specs

Posted on    5 mins read

As a JavaScript developer writing general-purpose libraries, you probably wish you could go fully universal, that is, you might want to create a library:

  • that can be used in the browser and in Node.js – without any hacks for either platform, and without code-duplication
  • that transparently utilizes AMD via RequireJS in the browser and CommonJS via require in Node.js – without any hacks for either platform, and without code-duplication
  • with a Jasmine spec suite which runs in the browser and in Node.js – without any hacks for either platform, and without code-duplication

Turns out this is perfectly possible. Consider the following library structure:


      lib/
        multiply.js

      spec/
        multiply.spec.js
  

As stated above: Without making use of any nasty hacks or duplicating any or all of our lib or spec code, we would like to be able to use our multiply.js module in a web-based browser application, or in a server-side application for Node.js – of course assuming that multiply.js provides a general-purpose functionality that is usable on both platforms, and not e.g. something working with a browser-DOM. In our example library, multiply provides a function that does the very useful job of multiplying two number, like this:


    console.log(multiply(2, 5)); // outputs "10"
  

Furthermore, when using our lib in the browser, we would like to be able to load it using RequireJS, and when using it in a Node.js application, we would like to be able to require() and use it just like any other npm module:


    // Browser script

    define("../lib/multiply", function(multiply) {
      console.log(multiply(2, 5));
    });
  


    // Node.js script

    var multiply = require("../lib/multiply.js");

    console.log(multiply(2, 5));
  

And we want our Jasmine specification multiply.spec.js to be able to run in a web-based Jasmine runner as well as in a Node.js based runner, enabling us to test our library on both platforms. Again, without duplicating the spec or lib code.

All this can be achieved with the following steps:

  1. Export the lib module and its spec via RequireJS’ describe logic
  2. For Node.js, add the amdefine package to make the lib and its spec loadable via Node’s require()
  3. For Node.js, add the jasmine-node package (but no node-specific RequireJS implementation!)
  4. For the client-side, add Jasmine and RequireJS
  5. For the client-side, write a Jasmine SpecRunner that uses RequireJS’ describe() to load the spec files for the test run

Once this is done, the library structure will look as follows:


      package.json

      lib/
        multiply.js

      spec/
        multiply.spec.js
        run.sh
        SpecRunner.html

      vendor/
        require.js

        jasmine/
          jasmine-html.js
          jasmine.css
          jasmine.js
          jasmine_favicon.png
          MIT.LICENSE
  

The /package.json file is the place where the amdefine and jasmine-node dependecy for the Node.js environment will be defined, /spec/run.sh is the Jasmine runner for the Node.js environment, /spec/SpecRunner.html is the Jasmine runner for the client-side environment, and /vendor hosts the external libraries RequireJS and Jasmine, again for the client-side environment.

Let’s tackle each file in turn:

/package.json:


{
  "name": "CafeGraph",
  "version": "0.0.1",
  "dependencies": {
    "amdefine": ">=0.0.2"
  },
  "devDependencies": {
    "jasmine-node": "1.0.x"
  }
}
  

This states that the npm modules amdefine and jasmine-node are needed for our lib to work in the Node.js environment. Run

npm install
to have them automatically installed.

/lib/multiply.js:


if (typeof define !== 'function') { var define = require('amdefine')(module) }

define([], function() {
  var multiply = function(a, b) {
    return a * b;
  };
  return multiply;
});
  

Here, several things happen that are crucial for our library to be universal. Because the module itself is wrapped in RequireJS’ define() function, it need some special treatment to be usable within a Node.js context. This is what the first line does, it makes sure that Node.js uses the define function of the amdefine package. This way, the module becomes usable just like any other npm package, even though RequireJS is not available.

The return multiply; statement has the same effect as module.exports = multiply; would have, making the multiply function available for Node.js scripts that do var multiply = require(“../lib/multiply.js”);

/spec/multiply.spec.js:


if (typeof define !== 'function') { var define = require('amdefine')(module) }

define(["../lib/multiply.js"], function(multiply) {
  describe("multiply", function() {
    it("multiplies two numbers", function() {
      expect(multiply(2, 5)).toEqual(10);
    });
  });
});
  

/spec/run.sh:


#!/bin/bash
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
node $DIR/../node_modules/jasmine-node/lib/jasmine-node/cli.js $DIR
  

This just a script which enables me to run the Jasmine test suite for the Node.js environment via ./spec/run.sh on the command line. It’s probably a totally unnecessary hack, but I never really found the time to find a better solution. Recommendations welcome. Not part of our code, thus it’s acceptable for now I think.

/spec/SpecRunner.html:


<!DOCTYPE html>
<html>
  <head>
    <title>Jasmine Test Runner</title>

    <!-- Jasmine -->
    <link rel="stylesheet" type="text/css" href="../vendor/jasmine/jasmine.css"/>
    <script type="text/javascript" src="../vendor/jasmine/jasmine.js"></script>
    <script type="text/javascript" src="../vendor/jasmine/jasmine-html.js"></script>

    <!-- RequireJS -->
    <script type="text/javascript" src="../vendor/require.js"></script>

  </head>

  <body>
    <script type="text/javascript">
      require.config({
        baseUrl: './'
      });

      require([
        '../spec/multiply.spec.js'
      ], function() {
        jasmine.getEnv().addReporter(new jasmine.TrivialReporter());
        jasmine.getEnv().execute();
      });
    </script>
  </body>
</html>
  

The web-based Jasmine spec runner needs several modifications, because the spec files need to be require()d the RequireJS way. However, once this is set up, the workflow for adding new code and specs is straight-forward. For adding new specs to the Node.js spec run, no additional steps are necessary. To include new specs in the web-based spec run, simply add the path to the spec file to the require() array, and that’s it.

You can find the complete library code on GitHub at https://github.com/manuelkiessling/universal-javascript-modules-example, with some additional usage examples.

tl;dr: Writing JavaScript modules that can be seamlessly included in client-side as well as server-side applications, and providing Jasmine test suites which allow to test these modules in a browser environment as well as a Node.js environment is possible without any dirty workarounds by writing these modules using the AMD pattern, and then using the amdefine package to make them available to Node.js.