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.
- Configure a partial in
spec/teaspoon_env.rb
config.suite do |suite|
suite.boot_partial = '/boot_system_js'
end
- 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);
});
- Precompile
spec_helper.js
Teacup requiresspec_helper.js
to be in scripts to precompile.
Enable it just for test and development environment by addingconfig.assets.precompile += %w( spec_helper.js )
toconfig/environments/development.rb
andconfig/environments/test.rb
- Fire up
rails server
and openhttp://localhost:3000/teaspoon/default
.
You should see error in console:GET http://localhost:3000/assets/greeter.js 404 (Not Found)
.
If you seegreeter.js.js
you are not drunk, just missed a spot in install jspm (about adding"*.js": "*.js"
). - Now to fulfill the failing test, you should create
assets/greeter.js
.
After creating empty file, you can runrake 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
- 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.
rm assets/builds/*.js
- Do your changes. Try it in the browser.
- Commit your changes.
git status
will complain aboutassets/builds/*
jspm bundle welcome.js assets/bundles/welcome.js
(or codify it as a rake task)- 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
- jspm and SystemJS – Guy Bedford
- babel – Sebastian McKenzie
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