Babel + UMD = BumbleD. Incrementally migrate your Sprockets-powered javascript to ES6 modules by transforming them to UMD modules that preserve your existing global references.
ES6 modules are the future. 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 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 (and depending on other globals) 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 provides this with a Sprockets transformer that acts on
.es6
files. These files are transpiled by Babel and the
ES2015 -> UMD modules transform
plugin, preserving any globals that you've registered.
- 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-es2015-modules-umd babel-preset-es2015
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.
- Note: Sprockets::BumbleD requires babel-core version 6.22.0 or higher.
This is because it can be used to transpile assets provided by a gem (e.g.
a rails plugin), and those assets would exist outside your application
subtree (they'd exist where your gems are globally installed), so we rely
on the
resolvePlugin
andresolvePreset
methods introduced in PR #4729.
- 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).
In config/application.rb
:
extend Sprockets::BumbleD::DSL
configure_sprockets_bumble_d do |config|
config.root_dir = File.expand_path('..', __dir__)
config.babel_config_version = 1
end
The root_dir
setting is required! This tells Sprockets::BumbleD the
directory from which node modules are to be resolved (typically, wherever your
package.json resides). If that's Rails.root.to_s
, use that. If it's in a
specific subdirectory, specify that. Sprockets::BumbleD doesn't care, as long
as its node require
statements will resolve from that directory.
By default you get
babel-preset-es2015,
babel-plugin-external-helpers,
and
babel-plugin-transform-es2015-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-es2015-modules-umd is always 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.root_dir = File.expand_path('..', __dir__)
config.babel_config_version = 2
config.babel_options = {
presets: ['es2015', 'react'],
plugins: ['external-helpers', 'custom-plugin']
}
end
You can specify any options that are allowed in a .babelrc
file.
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.
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.
As of version 6.12.0, babel-plugin-transform-es2015-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
this PR). If you're using an older
version of the plugin, upgrade to at least 6.12.0 to get the full value of this
gem and registering globals.
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;
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.
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.
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: ['es2015'],
plugins: ['external-helpers', 'transform-es2015-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