Sprockets::Commoner
⚠️ This gem is unmaintained. I advise you to use Webpacker in any new project ⚠️
Sprockets::Commoner is a gem that enables JavaScript package and Babel transformation in Sprockets. It is meant as a replacement for Browserify or Webpack in Rails.
Features
- Compile JavaScript modules into a single bundle.
- Run Babel transforms.
- Easy setup. We adhere to the Rails Way™ and don't require any additional configuration for the simplest and most common set ups.
- Integrate tightly into Sprockets/Rails. You can use any of Sprockets' features like ERB inside your files without having to jump through crazy hoops.
- Designed to emit code that compresses incredibly well. The code generated will be smaller than webpack or browserify.
- Use
process.envinside your JavaScript. This will also run on anything innode_modules(e.g. Babel), to ensure dependencies are also compressed optimally. - Automatic deduplication of Babel helpers. No need to use
babel-runtime, as commoner will automatically detect which helpers are used and share them between modules in a way that compressed very well.
Setup
Requirements
- Ruby v2+.
- Rails 4+/Any other application that uses Sprockets 3+.
- NPM v3+. We only support version 3 because commoner doesn't do any sort of deduplication of dependencies, so you could end up with a huge bundle if you don't want out. We only test against version 3, so you will definitely run into issues when running version 2.
- We recommend and support version 4+ of Node.js.
10-second simple set up
To get started, let's begin with the simplest possible set up: just enabling resolving of require.
- Add
sprockets-commonerto Gemfile, runbundle install, and restart your Rails server. - Add package.json with
babel-coreversion 6 and any packages you want. For the example, we'll use the excellent lodash library.npm install -S babel-core@6 lodash -
requireyour client-side JavaScript packages fromapplication.js!
var _ = require('lodash');
console.log(_.map([1, 2, 3], function(n) { return n * 3; }));Enabling Babel transforms
- Install any Babel plugins or presets you want to use. We'll use the default ES2015 preset;
npm install babel-preset-es2015. - Add a
.babelrcwith you required configuration. We just need to doecho '{"presets": ["es2015"]}' > .babelrc. - Use any feature you want! For example, let's use
importand arrow functions in ourapplication.js:
import {map} from 'lodash';
console.log(map([1, 2, 3], (n) => n * 3));Advanced configuration
Fine-tuned selection of files to process
By default, commoner will process any file under the application root directory. If you want more fine-tuned control over which files to process, you can specify which paths to include or exclude. To do so, you will need to re-register the Sprockets processor. For example:
# In config/initializers/sprockets_commoner.rb
Rails.application.config.assets.configure do |env|
Sprockets::Commoner::Processor.configure(env,
# include, exclude, and babel_exclude patterns can be path prefixes or regexes.
# Explicitly list paths to include. The default is `[env.root]`
include: ['app/assets/javascripts/subdirectory'],
# List files to ignore and not process require calls or apply any Babel transforms to. Default is ['vendor/bundle'].
exclude: ['vendor/bundle', /ignored/],
# Anything listed in babel_exclude has its require calls resolved, but no transforms listed in .babelrc applied.
# Default is [/node_modules/]
babel_exclude: [/node_modules/]
)
endTesting with Teaspoon
Teaspoon is not compatible with commoner by default. Under test/demo you can find a demo Rails application, containing a setup with Teaspoon. The key change is a gem called teaspoon-bundle, which contains a custom boot partial. Just add teaspoon-bundle to your Gemfile, add suite.boot_partial = 'bundle_boot' to your teaspoon_env.rb, and you should be good to go!
CoffeeScript interoperability
Commoner is designed from the start to facilitate a transition from CoffeeScript to ES2015.
Importing CoffeeScript files
Any JavaScript file can require a CoffeeScript file, which will cause that CoffeeScript file to be scanned for a global variable reference and the require call to be replaced with a reference.
If we have the following two files:
# file.coffee
class window.ImportantClass// main.js
var klass = require('./file');
new klass();Then the second file will just be compiled down to new window.ImportantClass(). Importing global references works for global assignments and class definitions.
Expose
We have added a custom directive that makes it very easy to expose an ES2015 module to the global namespace so it can be used by CoffeeScript files or any other code. For example:
'expose window.MyClass';
export default class MyClass {}expose will use the default export if available, otherwise the whole module namespace will be assigned to the global variable. For example:
// constants.js
'expose window.Constants';
export const A = 1;
export const B = 2;
export const C = 3;This will make window.Constants equal {A: 1, B: 2, C: 3}.
Methodology
Commoner registers a postprocessor that takes any application/javascript file and passes it through Babel.
require support
Sprockets::Commoner enables support for require() by replacing require function calls with variable references. It does this by assigning every file a unique identifier based on their filename. For example, if the file you're referencing is located at <root>/node_modules/package/index.js it will get the name __commoner_module__node_modules$package$index_js. Any file that then requires this specific file will have that require call replaced with the identifier.
reference.
The Babel plugin also communicates back any files that were required to make sure they are included by Sprockets. Sprockets::Commoner depends on the topological ordering that Sprockets does to make sure any module that is needed is instantiated before it is used.
Example
After the regular Babel plugins are done doing their thing, babel-plugin-commoner-internal kicks in, which is commoner's plugin that does the actual resolving. This plugin does the following things:
- It finds any
requirecalls and rewires them to variable references (as detailed inrequiresupport) - It wraps the module in a function and supplies it with
moduleandexports. The end value ofmodule.exportsgets assigned to the module identifier, which is referenced by other files (as specified in 'requiresupport') Example:
var __commoner_module__node_modules$package$index_js = __commoner_initialize_module__(function (module, exports) {
exports.default = 123;
});- If these is an expose directive at the top of the file, it assigns
module.exportsto the specified variable. For example, if the top of the file contains'expose window.Whatever';it will assignexports.defaultif there is a default, otherwise it just assignsexports. Therefore if our file hasexpose window.Whateverand no default, it will getwindow.Whatever = exports;appended to it.
Bundling
After all the files have been transformed, there is a bundle step which combines all of the processed JavaScript modules together. It then prepends an initializer function and all the Babel helpers (which are shared between all modules).
For example, if we have the following two files:
// module.js
export default function a() {
return 1;
};// application.js
import a from './module';
a();We will end up with the following (browser-runnable) file:
!function() {
var __commoner_initialize_module__ = function(f) {
var module = {exports: {}};
f.call(module.exports, module, module.exports);
return module.exports;
};
var global = window;
var __commoner_helper__interopRequireDefault = function (obj) {
return obj && obj.__esModule ? obj : {
default: obj
};
};
var __commoner_module__app$assets$javascripts$module_js = __commoner_initialize_module__(function (module, exports) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = a;
function a() {
return 1;
};
});
var __commoner_module__app$assets$javascripts$application_js = __commoner_initialize_module__(function (module, exports) {
'use strict';
var _module2 = __commoner_helper__interopRequireDefault(__commoner_module__app$assets$javascripts$module_js);
(0, _module2.default)();
});
}();This file is meant to be compressed, and does incredibly well when processed by UglifyJS.