Sprockets::BumbleD
This gem provides a plugin for Sprockets that enables you to transpile modern javascript using Babel.
A primary use case for this gem is to incrementally migrate your Sprockets-powered javascript codebase to ES6 modules. This works by transforming them to UMD modules that preserve your existing global variable references (hence the name: Babel + UMD = BumbleD). Once an entire subtree of your javascript module tree is written in ES6 modules, this frees you up to bundle that javascript using a more modern tool (e.g. rollup or webpack).
That said, this gem can be used for general purpose babel transpilation within the Sprockets pipeline.
Background
ES6 modules are the new standard. The syntax is great: it's concise and
straightforward, and the static and explicit nature of import
and export
statements make your code a complete spec of its dependencies and how to
resolve them. This means that moving to ES6 modules also makes moving away
from Sprockets //= require
directives for javascript bundling (and Sprockets
in general) much easier.
But when faced with a large legacy codebase, it's not feasible to convert everything to ES6 modules at once. Thus, the goal is to be able to convert module-by-module from explicitly exporting a global variable (and depending on other modules' global variables) to following the ES6 module format, which we'll then transpile to UMD that is compatible with non-converted code (e.g. existing UMD modules and plain old global-dependent scripts).
Sprockets::BumbleD accomplishes this goal by providing a Sprockets transformer
that acts on .es6
files (this file extension is
configurable). These files are transpiled by
Babel and the ES2015 -> UMD modules transform plugin, preserving any globals
that you've registered.
Setup
Installation
- Add
gem 'sprockets-bumble_d'
to yourGemfile
(or add a gemspec dependency to an inline engine in your app) and runbundle install
. - Run
npm install --save @babel/core @babel/plugin-external-helpers @babel/plugin-transform-modules-umd @babel/preset-env
to install the modules for the default babel config. If you want to customize the babel options, install any additional plugins and presets you want. - Generate the external helpers and
//= require
them in at the beginning of your application manifest or pull them in with a separate script tag. This step is of course unnecessary if you won't be using the external-helpers plugin, but it's highly recommended that you do (to avoid inlining them everywhere, which unnecessarily bloats the bundle sent to the browser).
Basic configuration
In config/application.rb
:
extend Sprockets::BumbleD::DSL
configure_sprockets_bumble_d do |config|
config.babel_config_version = 1
end
If you are not using Rails, you must also configure the root_dir
(see below).
Customizing the root_dir
Sprockets::BumbleD needs to know the directory from which node modules are to
be resolved (typically, wherever your package.json
resides). If you're using
Rails, this defaults to Rails.root.to_s
. If you are not using Rails, or if
your node_modules folder is not inside Rails.root
, you must configure the
root_dir
setting! For example, if you are configuring Sprockets::BumbleD in
the file config/application.rb
and your package.json
is located in the
parent directory, use:
configure_sprockets_bumble_d do |config|
config.root_dir = File.expand_path('..', __dir__)
config.babel_config_version = 1
end
If it's in a specific subdirectory, specify that directory instead.
Sprockets::BumbleD doesn't care, as long as its node require
statements will
resolve from that directory.
Customizing your babel options
By default you get @babel/preset-env, @babel/plugin-external-helpers, and
@babel/plugin-transform-modules-umd. If you want to customize this with
different plugins and presets, specify them in the
configure_sprockets_bumble_d
block with the babel_options
setting. Note
that (because it's central to the purpose of this gem)
@babel/plugin-transform-modules-umd is included for you (unless
you set transform_to_umd
to false
)
and configured to use the registered globals, so this
plugin does not need to be specified when you override the default plugins.
For example:
configure_sprockets_bumble_d do |config|
config.babel_config_version = 2
config.babel_options = {
presets: ['@babel/preset-env', '@babel/preset-react'],
plugins: ['@babel/plugin-external-helpers', 'custom-plugin']
}
end
You can specify any options that are allowed in a .babelrc
file.
Customizing the file extension
By default the Sprockets transformer is registered to act on .es6
files. This
is configurable:
configure_sprockets_bumble_d do |config|
config.babel_config_version = 1
config.file_extension = '.babel'
end
The babel_config_version
setting
What's this mysterious babel_config_version
we're setting in the previous
examples? Good question. Essentially this is intended to be a value that
translates to the composite version of @babel/core and each babel preset
and plugin in your application. It's used to expire the cache for compiled
assets: since different versions of babel and its plugins can result in a
different transpiled output, we want to be able to invalidate the cache
whenever we change our babel configuration. So, when you upgrade @babel/core
or you add/remove/upgrade a babel plugin or preset, you'd increment this
version which will cause the Sprockets transformer's cache key to change.
Philosophy
You should own your babel setup. We want to be able to use the latest versions
of babel and its plugins as soon as they're available, so this gem doesn't
vendor any node modules - it's up to the application to provide those to the
gem. This is what the root_dir
config is for. It's also why the
babel_config_version
setting exists.
Registering globals
@babel/plugin-transform-modules-umd includes an exactGlobals
option that lets
you specify exactly how to transpile any import statements into the global
reference it should resolve to. It also lets you specify what global should be
exported by an ES6 module in the resultant UMD output. (A complete description
is available in babel PR #3534.)
In config/application.rb
, after extend Sprockets::BumbleD::DSL
:
register_umd_globals :my_app,
'my/great/thing' => 'MyGreatThing',
'her/cool/tool' => 'herCoolTool'
Doing this will allow:
import GreatThing from 'my/great/thing';
to be transpiled to:
factory(/* ... */ global.MyGreatThing);
in the globals branch of the transpiled UMD output. Similarly, the above map
also specifies that the exports of the ES6 module her/cool/tool
will be
assigned to the herCoolTool
global.
That is, registering these globals provides both:
- a way to depend on existing globals in ES6 modules
- a way to declare the global an ES6 module should export, to be used in existing UMD modules or direct global references
As a corollary, if you are writing a new ES6 module that is only used by other ES6 modules, you would not need to register a global for that module's export.
Exported globals can also be nested objects and the transform will properly handle creating the necessary prerequisite assignments. For example with this registration:
register_umd_globals :my_app,
'her/cool/tool' => 'Her.Cool.Tool'
the compiled her/cool/tool
module will contain:
global.Her = global.Her || {};
global.Her.Cool = global.Her.Cool || {};
global.Her.Cool.Tool = mod.exports;
Inline Rails engines
If you have a large application, you may have split it into multiple inline rails engines (as described in this talk). Inline engines with their own assets should own the registration of globals for these assets. This is supported in Sprockets::BumbleD:
in some_engine/engine.rb
:
extend Sprockets::BumbleD::DSL
register_umd_globals :some_engine,
'some_namespace/first_module' => 'SomeNamespace.firstModule',
'some_namespace/second_module' => 'SomeNamespace.secondModule',
'another_thing/mod' => 'anotherModule'
Since module globals should only be registered in the engine (or top level
application) where the module lives, register_umd_globals
will raise
Sprockets::BumbleD::ConflictingGlobalRegistrationError
if a module is
registered a second time. Of course, this still can't prevent you from
registering globals (that had not already been registered) in the wrong engine.
Reminder about Rails reloading
As with any config
changes, updates to the globals registry are not
reloaded automatically; you must restart your server for the changes to take
effect.
Do I have to transpile to UMD modules?
No, you can transpile to other module formats (e.g. AMD). You'd just be using
less of this gem's API surface area 1. You can set transform_to_umd
to
false
in your configure_sprockets_bumble_d
block, and
override the default plugins to use a
different module transform. For example if you're using an AMD loader like
almond, you could configure modules to be transpiled to AMD like so:
configure_sprockets_bumble_d do |config|
config.root_dir = File.expand_path('..', __dir__)
config.babel_config_version = 1
config.transform_to_umd = false
config.babel_options = {
presets: ['@babel/preset-env'],
plugins: ['@babel/plugin-external-helpers', '@babel/plugin-transform-modules-amd']
}
end
You can reference the 5.0_amd test app which demonstrates this in a full application.
1 Of course if you're doing this, you wouldn't ever call
register_umd_globals
Similar projects
- babel-schmooze-sprockets - This takes a similar approach, but it requires Sprockets 4 (which is still in beta), and it doesn't offer a way to register globals within inline engines. Additionally, it diverges in philosophy by vendoring some node_modules.
-
sprockets-es6 - This was the common solution for ES6 transpilation within
Sprockets for a while, but it takes a very different approach. Instead of
relying on node and the npm ecosystem, it uses ruby-babel-transpiler, which
is stuck on babel 5. This means you cannot configure custom babel plugins
(which means you can't use
exactGlobals
to specify what it should transform globals to in the UMD modules transform). - sprockets 4 - This takes the same approach as sprockets-es6 so it suffers from the same limitations as sprockets-es6