ES2015: A SystemJS Odyssey

in Development

On the Code School course team, we’re always looking for ways to improve our course platform, Campus. One job of this platform is to validate code in the browser. To do this, we use the Mocha JavaScript testing framework and a client executor that runs a suite of unit tests against the user-submitted code. This executor has served our needs for the last two years, but had some issues that we wanted to address. So today I’m going to walk through how and why we made the jump and revamped our executor.

The first issue was that all the support code needed to validate challenge answers gets delivered to the client together. This is great if you’re on a fast connection, but could lead to a long download time if your connection isn’t the best. If you’re on a multiple-choice challenge, you could be stuck downloading megabytes of JavaScript code when all you really need is a few lines. So, we wanted to deliver the answer validation code in a more efficient manner.

The second issue was that our client executor was never used to transpile code, since our previous courses only used ES5. But because our ES2015 course, ES2015: The Shape of JavaScript to Come was on the horizon, we needed a way to transpile ES2015 code.

Improving Code Delivery

We decided to tackle the code delivery issue first, since it had been an issue from the beginning of the Campus platform.

  • We wanted a solution that would meet the following requirements:
  • Download only the required JavaScript for each challenge.
  • Allow us to continue using npm as our package manager.
  • Minimize our maintenance overhead.

Determining the Required JavaScript

Figuring out which JavaScript dependencies a set of tests needs wasn’t a simple process in the Campus platform. Why? Because the tests are contained inside a domain-specific language (DSL) used to describe courses, and the actual tests aren’t sent to the client until a code submission is made.

Take a sample challenge’s CommonJS imports — it should only download chai, jquery, and lodash:

var assert = require('chai').assert,
 $ = require('jquery-browserify'),
 _ = require('lodash');

Whereas this multiple-choice challenge should only download chai:

var assert = require('chai').assert,
 choice = code[0];

It’s pretty straightforward for humans to look at these tests and figure out what’s required. We could write code to scan for all tests in the DSL and create a mapping of required modules, but this wouldn’t be an easy task to accomplish. In addition, we would need to maintain this mapping as our tests change, so it would add to our maintenance overhead. We needed a way to load modules at runtime based on the test code.

Enter SystemJS

Luckily, SystemJS does exactly what we need. It’s described as a universal dynamic module loader. That’s a mouthful, so let’s break it down.

Universal

This means that it understands multiple module formats. The current formats are:

  • esm: ECMAScript Module
  • cjs: CommonJS
  • AMD: Asynchronous Module Definition
  • global: Global shim module format
  • register: SystemJS’s internal module format

Most of the libraries we use at Code School are already in the CommonJS format, but enabling us to use libraries in other module formats is a bonus.

Dynamic

SystemJS uses static analysis to examine the code for dependencies. It first determines the module format used, then the modules included, and loads the modules accordingly.

Module Loader

Module loaders load modules. They typically come in two varieties — synchronous and asynchronous. CommonJS, the module system Node.js uses, is a synchronous module loader. RequireJS is an example of an asynchronous module loader. SystemJS can act as both.

It’s pretty easy to tell synchronous module loaders from asynchronous modules loaders. Take a look at these two module imports for jQuery:

// CommonJS
var $ = require('jquery');
$('button').click();

// RequireJS
define(['jquery'], function($) {
  $('button').click();
});

These both do the same thing — load jQuery as a module. However, with the first one, jQuery has been bundled for use in the browser using Browserify and is already loaded in the browser. This is why it can be assigned to a variable right away. The second example doesn’t use bundling and makes an HTTP request for the jQuery module. Once the jQuery module is loaded, the callback is invoked with the module.

You might be wondering how SystemJS acts synchronously and asynchronously. The answer is that each module can use either synchronous or asynchronous dependencies. Take the following code used to load an application:

<script src="system.js"></script>
<script src="config.js"></script>
<script>
  System.import('app.js').then(function(app) {
    app.start();
  });
</script>

This is SystemJS’s way of loading app.js asynchronously. Once the file is downloaded, it analyzes the code for dependencies, and issues HTTP requests for those dependencies. It doesn’t actually run app.js’s code until all its dependencies are loaded. This means that app.js can use synchronous imports, like our first example:

var $ = require('jquery');
$('button').click();

Delivery Solved

Now that we have a dynamic module loader, the solution to delivering only the code required to validate a specific challenge becomes:

  1. Inject the challenge’s tests code as a module, named tests.
  2. Load the tests module.
  3. Run the tests.

Injecting the test code at runtime was easier said than done. Luckily, SystemJS is an implementation of the ES2015 module loader specification, which provides the following customizable module loading hooks:

  • normalize : Provides the full module name. This would be useful if a module is aliased.
  • locate : Provides the full location of the module. For a file system, this might be an absolute path. For the web, this would be the URL of the module file.
  • fetch : Retrieves the module file.
  • translate : Performs any code translation required.
  • instantiate : Creates the module by executing the translated code.

These hooks did not make it to the final ES2015 specification and have since been moved to a new module loading specification. For now, SystemJS still provides an implementation of the specification, so we took advantage of it.

Because our test code is provided by Campus at runtime, the fetch hook needed to be redefined so that instead of trying to load the tests module from a URL, it could be injected into the module loading process. In order to do this, we provided our own implementation of System.fetch():

// Customizing System.fetch so that we can inject the test code
// at runtime, and have System.js evaluate it for dependencies.
var systemFetch = System.fetch;
System.fetch = function(load) {
  if (System.normalizeSync('tests') === load.name) {
    return new Promise(function(resolve, reject) {
      resolve(tests);
    });
  }
  return systemFetch.apply(this, arguments);
};

Our custom implementation checks to see if the tests module is being loaded and uses a Promise to pass the tests module’s code back to SystemJS. This is done by passing the test code, represented by the variable tests, to the promise’s resolve() method. Once passed back to SystemJS, the code is analyzed for its dependencies. All other modules are loaded using the original implementation of System.fetch().

The next part is telling SystemJS how to load everything else, which is where jspm comes in.

JSPM

Jspm is a package management tool similar to npm, but it focuses on making dependencies available to the browser.

It accomplishes this by first providing registries that can be used to download packages. The two registries provided are npm and GitHub, but additional ones can be added. This meets our second requirement of being able to continue using npm for our package manager.

Once jspm downloads the package, it places it in a known folder structure and configures SystemJS so that it knows how to make HTTP requests for dependencies. All that’s left was exposing this folder structure over HTTP. Code School’s Campus platform already has a way to host course specific files, so that part was easy.

Packaging

Now that our module dependencies are available over HTTP, we have some decisions to make. As mentioned, we previously bundled everything together using Browserify. Now that we’re using jspm, we get to choose between a few more options:

  • Self-executing bundle : This is similar to how most JavaScript applications are delivered. Everything is bundled into a single file, and can be loaded with a <script> tag.
  • No bundling : Everything is loaded individually. This can generate hundreds of cascading requests for a complex dependency tree.
  • Dependency Cache : This creates a lookup table of the dependency tree so that multiple HTTP requests can be made in parallel. This is mainly a performance improvement if you’re not going to bundle at all.
  • Injectable bundles : This groups several dependencies into a bundle, and then “injects” configuration so that SystemJS knows what the bundle contains. When a module within that bundle is requested, SystemJS loads the bundle.

Using the self-executing bundle would be similar to the delivery method the Campus platform already uses. Remember that our goal was to deliver only the required JavaScript to the client for a specific challenge. The only way this method could work is if we created a self-executing bundle for each challenge’s tests.

While we’re developing a course, the easiest option is no bundling. This means that we can make changes to code without needing a build process. However, the number of HTTP requests made by SystemJS quickly grows out of hand. This won’t be a good user experience once the course is deployed.

The next best thing is to use injectable bundles. This bundles multiple dependencies together, but still loads the bundle at runtime. We just needed a way to create bundles to meet the needs of different tests. Luckily the jspm bundle command uses bundle arithmetic.

Bundle Arithmetic

This is the last part of our code delivery puzzle. To explain, I’ll use an example from Code School’s ES2015: The Shape of JavaScript to Come. In this course, our editor challenge tests wound up using a combination of techniques, requiring a different mix of dependencies.

The first, and most widely used, is parsing the submission into an abstract syntax tree (AST) and analyzing it to verify that certain keywords or syntax were used.

The second technique is executing the code submission inside a JavaScript sandbox and spying on method calls or examining the runtime values.

Here’s an example of the imports needed to run AST checks for a submission:

import analyze from 'analyze';
import {assert} from 'chai';
import esquery from 'esquery';
import * as rules from 'rules';
import extend from 'extend';

And this is an example of the imports needed to run both AST checks and execute the submitted code in a sandbox:

import analyze from 'analyze';
import {assert} from 'chai';
import esquery from 'esquery';
import * as rules from 'rules';
import escodegen from 'escodegen';
import Sandbox from 'sandbox';
import sinon from 'sinon';

As you can see, there’re some common dependencies between these tests. Ideally, we’d want to create a bundle for common dependencies, and then smaller bundles for AST-based tests and sandbox-based tests. This is where bundle arithmetic shines.

Jspm’s bundle command uses bundle arithmetic to build custom bundles. As an example, here’s the command used to create a common bundle for the two test types:

jspm bundle 'static-analysis.js & static-analysis-with-sandbox.js' common-bundle.js --inject

The & is a bundle arithmetic operator used to find the common dependencies between two files. The output of this command is a bundle containing only the analyze, chai, esquery, and rules modules.

The + and – operators are also supported, which allows us to build the other two bundles we want. This command builds the bundle for AST-based tests:

jspm bundle 'static-analysis.js - common-bundle.js' static-analysis-bundle.js --inject

We’re telling jspm to subtract modules contained in common-bundle.js from the modules needed for static-analysis.js. The resulting bundle will contain only the extend module. Similarly, this command builds the bundle needed for sandbox-based tests and contains only escodegen, sandbox, and sinon:

jspm bundle 'static-analysis-with-sandbox.js - common-bundle.js' static-analysis-with-sandbox-bundle.js --inject

Now we’re delivering the code needed to verify our tests in a way that minimizes download size and the number of HTTP requests. Phew!

Transpilation

The next issue was being able to execute user submissions in ES2015 in the browser. Most browsers don’t support all of ES2015’s language features, so the code must first be translated into ES5 code before attempting to run it — this process is called transpilation.

Even though we originally used SystemJS to solve our code delivery issue, it turns out one of its main features is transpiling code. All that’s required is adding some configuration that tells it which transpiler to use. The jspm init command makes this easy by prompting you when setting up a new project, and gives you the choice of using either of the well-known transpilers, Babel or Traceur.

We decided to use Babel for our ES2015: The Shape of JavaScript to Come course. There’re two options for executing the user’s code — the first is to use Babel’s API directly to transform the code into ES5, and then run it inside a JavaScript sandbox. This option works well for code that’s not already contained within a module. The second option is used when the user’s code is already using ES2015’s module features. In this case, we register the code as a module with a SystemJS sandbox, and it transpiles the code during the translate hook in the module loading process.

An interesting side effect of our code delivery solution is that our tests could also be written using ES2015. Because our test code is injected during the fetch hook, which comes before the translate hook, SystemJS will automatically detect that we’re using ES2015 code and transpile it for us. How’s that for practicing what we teach?

Try It Yourself

SystemJS and jspm happen to be great solutions to two very specific problems we wanted to solve at Code School. This doesn’t mean you can’t try it yourself. Here is a great video showing how to get started developing a web app with ES2015 and jspm.

Want to see the solutions described here in action? Head over to our ES2015 tutorial, ES2015: The Shape of JavaScript to Come, , and hone your ES2015 skills. Have any suggestions or feedback? I’d love to hear them in the comments below (tip: click “View Discussion”)!

Code School

Code School teaches web technologies in the comfort of your browser with video lessons, coding challenges, and screencasts. We strive to help you learn by doing.

Visit codeschool.com

About the Author

Russ Centanni

I’m a course developer at Code School that also enjoys making beer, gardening, and outdoorsy stuff.

Might We Suggest