From the buzz on Twitter and blog posts, you could feel that ECMAScript 6 was finally coming. It has many things we’ve wanted for years, so it makes sense to start new projects with it in mind.

ECMAScript 6

Others have written in depth about various ECMAScript 6 features. I’d like to focus just on one: module loading. There is no common way to load your ES6 modules natively in the browsers. For example babel, has support for three different module loaders. There was a System dynamic module loader included in the ES6 specification, but in the end it was removed and work continued as WhatWG loader spec. Yes, you can define modules, classes, export them, and import them, but there is no way how to load them across files. This also means that the import {} from 'file.js' does not work.

However, we can use System module loader now, via awesome polyfill es6-module-loader. On top of that, there is SystemJS – universal dynamic module loader which loads basically everything you can think of: ES6 modules, AMD, CommonJS and global scripts in the browser and NodeJS.

It sounds like we could actually use those ECMAScript 6 modules now thanks to SystemJS.

jspm

Frictionless browser package management

  • jspm is a package manager for the SystemJS universal module loader, built on top of the dynamic ES6 module loader
  • Load any module format (ES6, AMD, CommonJS and globals) directly from any registry such as npm and GitHub with flat versioned dependency management. Any custom registry endpoints can be created through the Registry API.
  • For development, load modules as separate files with ES6 and plugins compiled in the browser.
  • For production (or development too), optimize into a bundle, layered bundles or a self-executing bundle with a single command.

If you’ve used tools like browserify or webpack, you know how it is to run a precompiler, add options to the compiler when you want to use JSX and do other chores. With jspm, the experience is very different. You install and initialize it. And it works. In the browser! No process running on your machine is needed to compile things. See the guides on jspm.io for more details. I really recommend the talk from London React Meetup.

Rails

Yes, even in 2015 Ruby on Rails is still a thing. Unfortunately Rails tooling for ES6 is still very young – but hey, we’ve got jspm. These tools (sprockets-es6 for example) also require up to date (>= 3.0) sprockets, which is available on Rails 4.x. Some of us have to work with Rails 3 applications, so there has to be a way how to make it work even without server side compilation. You’d need a module loader with Rails 4 anyway because it is not part of the specification.

Rails + jspm

I’ll use Rails 4 in this example, but it really doesn’t matter which version it is. I’m also using latest stable version of jspm (0.15.7). You might run into problems that Rails 3 wants to minify your source maps or ECMAScript 6 files. We have solved that by naming them *.es6 and disabling source maps.

New Rails App

$ rails new jspm_example
$ cd jspm_example
$ rails generate controller welcome index --no-helper --no-assets

And you’ll have to add root route root 'welcome#index' to config/routes.rb.

Install jspm

If you don’t have jspm, install it as described in Getting Started.

$ npm install jspm -g

Then inside of the generated application, generate jspm configuration:

$ cd jspm_example
$ jspm init
Would you like jspm to prefix the jspm package.json properties under jspm? [yes]: yes
Enter server baseURL (public folder path) [./]: ./assets/
Enter jspm packages folder [assets/jspm_packages]:
Enter config file path [assets/config.js]:
Configuration file assets/config.js doesn't exist, create it? [yes]:
Enter client baseURL (public folder URL) [/]: /assets/
Which ES6 transpiler would you like to use, Traceur or Babel? [babel]: babel
ok   Verified package.json at package.json
     Verified config file at assets/config.js
     Looking up loader files...
       system.js
       system.src.js
       system.js.map
       es6-module-loader.src.js
       es6-module-loader.js
       es6-module-loader.js.map

     Using loader versions:
       es6-module-loader@0.16.6
       systemjs@0.16.11
     Looking up npm:babel-core
     Looking up npm:core-js
     Looking up npm:babel-runtime
     Updating registry cache...
ok   Installed babel as npm:babel-core@^5.1.13 (5.5.6)
     Looking up github:jspm/nodelibs-process
     Looking up github:jspm/nodelibs-fs
     Looking up github:systemjs/plugin-json
ok   Installed github:jspm/nodelibs-process@^0.1.0 (0.1.1)
     Looking up npm:process
ok   Installed npm:process@^0.10.0 (0.10.1)
ok   Installed babel-runtime as npm:babel-runtime@^5.1.13 (5.5.6)
ok   Installed github:jspm/nodelibs-fs@^0.1.0 (0.1.2)
ok   Installed github:systemjs/plugin-json@^0.1.0 (0.1.0)
ok   Installed core-js as npm:core-js@^0.9.4 (0.9.16)
ok   Loader files downloaded successfully

ok   Install complete.

Note that I used ./assets/ as public folder path and /assets/ as public folder URL (server by webserver). I think it is really nice to separate modern JS from its older forms to two very different folders. However, you can come up with your own convention like app/assets/jspm.

Now, there is a small issue with the latest stable version of jspm (0.15.7): it automatically adds .js to all loaded files. That means if you try to load ‘main’ it will load ‘main.js’. But also if you load ‘main.js it will try ‘main.js.js’. Fortunately, the fix is really easy. Add "*.js": "*.js", to “paths” in assets/config.js.

Next, let’s ignore jspm modules, generated assets and source maps from git.

$ echo '/assets/jspm_packages/*/**' >> .gitignore
$ echo '/public/assets/' >> .gitignore
$ echo '/assets/**/*.map' >> .gitignore

This will keep jspm and SystemJS in the git, so other developers won’t have to install jspm to just use the app in development environment.

And add our new assets folder as a load path to Rails Asset Pipeline.

$ echo "Rails.application.config.assets.paths << 'assets'" >> config/initializers/assets.rb

That way you’ll have nicely split assets managed by jspm (in ECMAScript 6) and normal Rails assets (together with your old javascripts).

Teaspoon

Because we are all agile, tests are needed for JavaScript too. And Teaspoon is a really nice toolkit to test your scripts. First add it to the Rails Gemfile.

group :test, :development do
  gem 'teaspoon-jasmine' # can be also -mocha or -qunit
end

Following Installation guide the next step is to initialize the project.

$ rails generate teaspoon:install
      create  spec/teaspoon_env.rb
       exist  spec/javascripts/support
       exist  spec/javascripts/fixtures
      create  spec/javascripts/spec_helper.js
+============================================================================+
Congratulations!  Teaspoon was successfully installed.  Documentation and more
can be found at: https://github.com/modeset/teaspoon

To make ES6 work in the tests, you need to initialize SystemJS in spec/javascripts/spec_helper.js.

Let’s create assets/jspm.js with following contents:

//= require jspm_packages/es6-module-loader.js
//= require jspm_packages/system.js
//= require config.js

And require it in spec/javascripts/spec_helper.js by replacing //= require application with //= require jspm. Because ES6 module system does not pollute global namespace and SystemJS allows to asynchronously load modules in the browser, you don’t need to load application.js. That would load all global jQuery or what you might end up having there.

PhantomJS

By default, Teaspoon is using PhantomJS. You get several options how to install it:

# on OSX (if you use Homebrew)
brew install phantomjs
# the rest
npm install -g phantomjs
# last resort
echo "gem 'phantomjs', group: :test" >> Gemfile
bundle install

greeter_spec.js

Let’s create sample test using ES6 modules. It will be a Greeter that will greet someone with Hello. Create spec/javascripts/greeter_spec.js with following contents:

import {Greeter} from 'greeter.js';

describe("Greeter", function() {
    const greeter = new Greeter();

    it('greets', function(){
        expect(greeter.greet('Someone')).toBe("Hello Someone!")
    });
});

But when executing tests, there is an syntax error:

$ rake teaspoon
Starting the Teaspoon server...
Teaspoon running default suite at http://127.0.0.1:54124/teaspoon/default
SyntaxError: Parse error



Finished in 0.00100 seconds
0 examples, 0 failures

Why? Because browsers do not support ES6 natively yet. Jspm to the rescue!

Teaspoon + jspm

Jspm uses SystemJS – asynchronous module loader. Notice the asynchronous? Teaspoon needs to execute loaded tests, but how does teaspoon know that the tests are loaded when it is asynchronous? The default is window.onload which does not work in this case as the scripts can be loaded after that event. It is very similar case to RequireJS with Teaspoon.

  1. Configure a partial in spec/teaspoon_env.rb
config.suite do |suite|
  suite.boot_partial = '/boot_system_js'
end
  1. Create the partial in spec/javascript/fixtures/_boot_system_js.html.erb
<%= javascript_include_tag @suite.helper %>

  Teaspoon.onWindowLoad(function () {
    System.register('teaspoon', , function() {
      return {
        setters: [],
        execute: function() { }
      }
    });

    System.import('teaspoon').then(Teaspoon.execute, Teaspoon.execute);
  });
  1. Precompile spec_helper.js
    Teacup requires spec_helper.js to be in scripts to precompile.
    Enable it just for test and development environment by adding config.assets.precompile += %w( spec_helper.js )
    to config/environments/development.rb and config/environments/test.rb
  2. Fire up rails server and open http://localhost:3000/teaspoon/default.
    You should see error in console: GET http://localhost:3000/assets/greeter.js 404 (Not Found).
    If you see greeter.js.js you are not drunk, just missed a spot in install jspm (about adding "*.js": "*.js").
  3. Now to fulfill the failing test, you should create assets/greeter.js.
    After creating empty file, you can run rake teaspoon to see how the test failure changes:
$ rake teaspoon
Starting the Teaspoon server...
Teaspoon running default suite at http://127.0.0.1:55128/teaspoon/default
F

Failures:

  1) Greeter encountered a declaration exception
     Failure/Error: TypeError: 'undefined' is not a constructor (evaluating 'new Greeter()') (line 12)

Finished in 0.00300 seconds
1 example, 1 failure

Failed examples:

teaspoon -s default --filter="Greeter encountered a declaration exception"
rake teaspoon failed
  1. Sweet. Let’s implement the Greeter.
export class Greeter {
    greet(person) {
        return `Hello ${person}!`;
    }
}

Execute the tests again and voila!

$ rake teaspoon
Starting the Teaspoon server...
Teaspoon running default suite at http://127.0.0.1:55161/teaspoon/default
.

Finished in 0.00300 seconds
1 example, 0 failures

jspm bundle

Now the part I like the most. There is no way I’m going to install NodeJS on our servers just to compile few assets. We can keep the bundled version in the repository and force people to keep track of it. As described in jspm’s Wiki Production Workflows, you can build bundles from modules and include all dependencies there. Let’s create simple application that will print our greeting to the console. Start by creating assets/welcome.js with following contents.

import {Greeter} from "greeter.js";
const greeter = new Greeter();

export function welcome(name) {
    alert(greeter.greet(name));
}

Let’s boot the rails server and open http://localhost:3000 to see if there are any errors. There shouldn’t be any, so continue with actually loading our welcome module and executing it. Add following snippet to app/views/welcome/index.html.erb:


  System.import('welcome').then(function(m){
    m.welcome('Someone');
  });

Refresh the browser and see Uncaught ReferenceError: System is not defined. Hmm. Right… Let’s load it in app/assets/application.js by replacing the whole file by just //= require jspm.

Remember jspm.js? It’s that little file that requires ES6 Module Loader and SystemJS. It was created when setting up teaspoon.

After refreshing the browser again, you should see alert dialog saying “Hello Someone!”. You might notice that it took a while.

What did we do here? Using System.import you can load any module exported by ES6. So here we import the welcome function and execute it. Modules should not have side effects, so just requiring them should not do anything. That’s why exporting a function is a good idea. Also, this way you can pass some parameters down to the modules, which is handy when you are integrating with existing system and not building single page apps.

Now to the Production Workflow. Let’s bundle some scripts.

$ mkdir assets/bundles
$ touch assets/bundles/.gitkeep
$ jspm bundle welcome.js assets/bundles/welcome.js
     Building the bundle tree for welcome.js...

       greeter.js
       npm:babel-runtime@5.5.6/core-js/object/define-property
       npm:babel-runtime@5.5.6/helpers/class-call-check
       npm:babel-runtime@5.5.6/helpers/create-class
       npm:core-js@0.9.16/library/fn/object/define-property
       npm:core-js@0.9.16/library/modules/$
       npm:core-js@0.9.16/library/modules/$.fw
       welcome.js

ok   Built into assets/bundles/welcome.js with source maps, unminified.

Now the last step is to actually load all files from app/bundles.

echo '//= require_tree ./bundles/' >> app/assets/javascripts/application.js

Ok. If you refresh the browser again, it should be much faster and not load scripts one by one.

Workflow

However, now you get precompiled versions all the time when you load it in the browser. There is simple workflow trick.

  1. rm assets/builds/*.js
  2. Do your changes. Try it in the browser.
  3. Commit your changes.
  4. git status will complain about assets/builds/*
  5. jspm bundle welcome.js assets/bundles/welcome.js (or codify it as a rake task)
  6. Let your CI to verify that assets/bundles/*.js is always the latest version.

You can also sprinkle it with some git pre-commit hooks to automatically verify it on development machine. There are two reasons why we want to have compiled copy in the git:

  • we don’t want to require Node on our production servers
  • some of our developers might not have Node on development machines

Unless you are a developer working on ES6, you don’t have to care about all the buzzwords and everything just works. Maybe time will prove it too hard to follow or too error prone, but the joys of simple deployment are greater than the risks.

People behind this

When you see them, give them a hug. They are doing awesome work to bring sanity into our development.

Source

You can find individual steps as commits in the mikz/rails-jspm-es6-example.

Last updated: September 19, 2023