EMBER RAILS TO EMBER CLI
A Mystery in 3 Parts
Me
Justin Giancola
Part I: How did this happen?
Stranded on ember rails
often have to deploy rails project to deploy ember changes (and vice versa)
development grinds to a screeching halt once ember app gets big enough
can't use awesome ember-cli plugins
asset dependency management is completely manual (as opposed to mostly manual for ember-cli)
Who is this talk for
people maintaining big ember_rails projects
reimplementing many things available in ember-cli addons
want to minimize risk in making transition to ember-cli
Part II: How do I escape?
Guidance
-
(read all of this)
(read naming-conventions section 3-10x)
-
- in the process of converting to ember-cli
Tools
sed and dirty, dirty regular expressions
Approaches
nuke (don't do this unless your project is very small)
hybrid (add es6 support to rails, move things over gradually or at once)
Gradual or band-aid?
nice to convert bit by bit
cutover from asset pipeline is always a big change
Setup
(add sprockets-es6-coffee if CoffeeScript)
install ember-cli and
ember-cli init
in Ember app's foldermodify app.js to use loader.js and add a custom resolver
Setup
// app/assets/javascripts/application.js
//= require handlebars
//= require ember
//= require ember-data
//= require loader
//= require ember-resolver
//= require ./resolver
var Resolver = require('your-app/resolver').default;
YourApp = Ember.application.create({
modulePrefix: 'your-app',
Resolver: Resolver
});
// app/assets/javascripts/resolver.js.es6
import Resolver from 'ember/resolver';
export default Resolver.extend({
normalize: function(fullName) {
var split = fullName.split(':', 2),
type = split[0],
name = split[1];
Ember.assert("Tried to normalize a container name without a colon (:) in it. You probably tried to lookup a name that did not contain a type, a colon, and a name. A proper lookup name would be `view:post`.", split.length === 2);
if (split.length > 1) {
var modulePrefix = this.namespace.modulePrefix;
var typePath = type + 's';
var moduleName = Ember.String.dasherize(split[1].replace(/\./g, '/'));
var modulePath = [modulePrefix, typePath, moduleName].join('/');
// First, try to find the module where ember-cli would locate it.
// If we succeed, use it.
if (requirejs.entries[modulePath]) {
return split[0] + ":" + moduleName;
}
}
// We don't have a module, so fall back to finding item defined via the app namespace.
if (type !== 'template') {
var result = name;
if (result.indexOf('.') > -1) {
result = result.replace(/\.(.)/g, function(m) { return m.charAt(1).toUpperCase(); });
}
if (name.indexOf('_') > -1) {
result = result.replace(/_(.)/g, function(m) { return m.charAt(1).toUpperCase(); });
}
return type + ':' + result;
} else {
return fullName;
}
}
});
- rejig es6_module_output to include module prefix ember-cli uses
# config/initializers/es6_module_transpiler.rb
prefix = 'your-project'
pattern = Regexp.new File.join(Rails.root, 'app/assets/javascripts')
ES6ModuleTranspiler.add_prefix_pattern pattern, prefix
- modify es6 compiler to be picky about filenames
# config/initializers/es6_module_transpiler.rb
prefix = 'your-project'
pattern = Regexp.new File.join(Rails.root, 'app/assets/javascripts')
ES6ModuleTranspiler.add_prefix_pattern pattern, prefix
ES6ModuleTranspiler.transform = lambda do |name|
name_parts = name.downcase.sub("#{prefix}/", '').split(/[^a-z]/)
has_suffix = name_parts.length > 1 && name_parts.first.chomp('s') == name_parts.last
bad_characters = name.match(/[^a-z\d\/\-]/)
if bad_characters or has_suffix
raise NameError.new %Q[Module name "#{name}" is incompatible with ember-cli naming conventions]
end
name
end
Status?
app still intact
can run, deploy, etc. exactly as before
ember-cli machinery now in place but not yet used
Part III: Escape from Alcatraz
Many Steps
pull dependencies into bower.json and Brocfile.js from all over hell's half acre
DO NOT UPDATE ANY DEPENDENCIES
make sure EVERYTHING pulled in IDENTICAL to what Rails project uses
I MEAN IT
Get rid of .erb
asset path interpolation now handled automatically
environment tests
// app/assets/javascripts/app/initializers/needs-env.js
import config from 'your-app/config/environment';
if (config.environment === 'test') {
// disable some things
}
- shell out to git for error reporting
Build or run app with
$ CURRENT_COMMIT=`git rev-parse --short HEAD` ember build
$ CURRENT_COMMIT=`git rev-parse --short HEAD` ember server
// app/assets/javascripts/config/environment.js
module.exports = function(environment) {
var ENV = {
CURRENT_COMMIT: process.env.CURRENT_COMMIT,
modulePrefix: 'your-app',
environment: environment,
baseURL: '/',
locationType: 'hash',
EmberENV: {
// ...
// app/assets/javascripts/app/initializers/error_reporting.js
import config from 'your-app/config/environment';
export default {
name: 'error-reporting',
initialize: function(container, app) {
var context = {
git: {
commit: config.CURRENT_COMMIT
}
};
// ...
Draw the rest of the fucking owl
Porting files
constants -> import statements
export in every file
relocate to path ember-cli expects
Gradual or band-aid?
one file at a time easier, more reliable (convert, test, release)
many files in parallel faster with good tests
Almost there
project now much like ember-cli project but running in rails
last renames (es6_module_transpiler and ember-cli not completely compatible)
run app through ember-cli for the first time (probably won't work)
Gotchas
can't share code from parent directories in project
submodules or custom ember-cli-addons instead
model factory injections
// app/assets/javascripts/app/app.js
Ember.MODEL_FACTORY_INJECTIONS = false;`
- setting to true can lead to weird errors, particularly if you are using polymorphic associations
- set back once everything works
- **note:** this will probably stop being an issue once either https://github.com/emberjs/data/pull/2316 or https://github.com/emberjs/data/pull/2345 is merged
- top level app constant disappears (only in production mode!)
// app/assets/javascripts/config/environment.js
module.exports = function(environment) {
var ENV = {
// ...
exportApplicationGlobal: true,
// ...
- remove ember-cli CSP addon until your project is ported
$ npm rm ember-cli-content-security-policy --save-dev
And then there were 2
- need to figure out developing, deploying 2 projects
The split
nginx or haproxy splits requests between apps
develop independently and bring together for deploy
static build into Rails public folder
Luke Melia's approach is quite good
proxy middleware for main ember app page
Proxy middleware
- install rack-streaming-proxy gem
# config/initializers/ember_cli_proxy.rb
if Rails.env.development?
require 'rack/streaming_proxy'
YourApplication::Application.configure do
config.streaming_proxy.logger = Rails.logger # stdout by default
config.streaming_proxy.log_verbosity = Rails.env.production? ? :low : :high # :low or :high, :low by default
config.streaming_proxy.num_retries_on_5xx = 0 # 0 by default
config.streaming_proxy.raise_on_5xx = true # false by default
config.middleware.insert_before ActionDispatch::Static, Rack::StreamingProxy::Proxy do |request|
base_url = 'http://localhost:4200/'
case request.path
when '/main-ember-entry-point'
base_url
when '/ember-cli-live-reload.js'
"#{base_url}ember-cli-live-reload.js"
end
end
end
end
// app/assets/javascripts/Brocfile.js
var EmberApp = require('ember-cli/lib/broccoli/ember-app');
var fingerprintOptions = {
enabled: true,
extensions: ['js', 'css', 'png', 'jpg', 'gif', 'svg', 'eot', 'woff', 'ttf']
};
var env = process.env.EMBER_ENV || 'development';
switch (env) {
case 'development':
fingerprintOptions.prepend = 'http://localhost:4200/';
break;
case 'test':
fingerprintOptions.prepend = '/main-ember-entry-point/';
break;
case 'production':
fingerprintOptions.prepend = '/main-ember-entry-point/';
break;
}
var app = new EmberApp({
fingerprint: fingerprintOptions,
// ...
Proxy gotchas
- fonts won't work unless you use CORS in ember-cli
$ npm install cors --save-dev
// app/assets/javascripts/student/server/index.js
module.exports = function(app) {
var globSync = require('glob').sync;
var cors = require('cors');
var bodyParser = require('body-parser');
var mocks = globSync('./mocks/**/*.js', { cwd: __dirname }).map(require);
var proxies = globSync('./proxies/**/*.js', { cwd: __dirname }).map(require);
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({
extended: true
}));
app.use(cors()); // <- THIS BIT
mocks.forEach(function(route) { route(app); });
// proxy expects a stream, but express will have turned
// the request stream into an object because bodyParser
// has run. We have to convert it back to stream:
// https://github.com/nodejitsu/node-http-proxy/issues/180
app.use(require('connect-restreamer')());
proxies.forEach(function(route) { route(app); });
};
- also need CORS config for production static deploy
- (you needed to do this anyway)
Everything is Wonderful
Except
holy shit Rails is slower than all hell
npm's fault
install elsewhere (or use symlinks)
DO NOT put 50k files in node_modules within app/assets/javascripts
Pro tips
big, parallel steps? many little commits
upstream rebases otherwise impossible
try to validate each change (as much as you can)
active generation not your friend in this case
sweeping changes more fun until debugging impossible
Thanks to
- Mattia Gheda @ghedamat