Skip to content

Latest commit

 

History

History
289 lines (232 loc) · 11.8 KB

README.md

File metadata and controls

289 lines (232 loc) · 11.8 KB

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

  1. Add gem 'sprockets-bumble_d' to your Gemfile (or add a gemspec dependency to an inline engine in your app) and run bundle install.
  2. 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.
  3. 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