EMBER RAILS TO EMBER CLI

A Mystery in 3 Parts

Me

Part I: How did this happen?

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

Tools

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

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

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

# 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

Thank You

justin@precisionnutrition.com