FEATURE: upgrade from therubyracer to mini_racer

This pushes our internal V8 JavaScript engine from Chrome 32 to 50.

It also resolves some long standing issues we had with the old wrapper.
This commit is contained in:
Sam 2016-05-19 22:25:08 +10:00 committed by Sam Saffron
parent f387dfe226
commit 695773db1c
7 changed files with 70 additions and 68 deletions

View file

@ -106,7 +106,8 @@ gem 'sidekiq-statistic'
# for sidekiq web # for sidekiq web
gem 'sinatra', require: false gem 'sinatra', require: false
gem 'therubyracer' gem 'execjs', github: 'rails/execjs', require: false
gem 'mini_racer'
gem 'thin', require: false gem 'thin', require: false
gem 'highline', require: false gem 'highline', require: false
gem 'rack-protection' # security gem 'rack-protection' # security

View file

@ -1,3 +1,9 @@
GIT
remote: git://github.com/rails/execjs.git
revision: 22476229323cbec3befa8b690cc6c7c3957a0044
specs:
execjs (2.6.0)
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
@ -94,7 +100,6 @@ GEM
erubis (2.7.0) erubis (2.7.0)
eventmachine (1.2.0.1) eventmachine (1.2.0.1)
excon (0.45.4) excon (0.45.4)
execjs (2.6.0)
exifr (1.2.4) exifr (1.2.4)
fabrication (2.9.8) fabrication (2.9.8)
fakeweb (1.3.0) fakeweb (1.3.0)
@ -144,7 +149,7 @@ GEM
librarian (0.1.2) librarian (0.1.2)
highline highline
thor (~> 0.15) thor (~> 0.15)
libv8 (3.16.14.13) libv8 (5.0.71.48.3)
listen (0.7.3) listen (0.7.3)
logster (1.2.3) logster (1.2.3)
loofah (2.0.3) loofah (2.0.3)
@ -159,6 +164,8 @@ GEM
method_source (0.8.2) method_source (0.8.2)
mime-types (2.99.1) mime-types (2.99.1)
mini_portile2 (2.1.0) mini_portile2 (2.1.0)
mini_racer (0.1.3)
libv8 (~> 5.0)
minitest (5.8.4) minitest (5.8.4)
mocha (1.1.0) mocha (1.1.0)
metaclass (~> 0.0.1) metaclass (~> 0.0.1)
@ -284,7 +291,6 @@ GEM
redis (3.3.0) redis (3.3.0)
redis-namespace (1.5.2) redis-namespace (1.5.2)
redis (~> 3.0, >= 3.0.4) redis (~> 3.0, >= 3.0.4)
ref (2.0.0)
rest-client (1.8.0) rest-client (1.8.0)
http-cookie (>= 1.0.2, < 2.0) http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 3.0) mime-types (>= 1.16, < 3.0)
@ -373,9 +379,6 @@ GEM
activesupport (>= 4.0) activesupport (>= 4.0)
sprockets (>= 3.0.0) sprockets (>= 3.0.0)
stackprof (0.2.9) stackprof (0.2.9)
therubyracer (0.12.2)
libv8 (~> 3.16.14.0)
ref
thin (1.6.4) thin (1.6.4)
daemons (~> 1.0, >= 1.0.9) daemons (~> 1.0, >= 1.0.9)
eventmachine (~> 1.0, >= 1.0.4) eventmachine (~> 1.0, >= 1.0.4)
@ -417,6 +420,7 @@ DEPENDENCIES
ember-rails (= 0.18.5) ember-rails (= 0.18.5)
ember-source (= 1.12.2) ember-source (= 1.12.2)
excon excon
execjs!
fabrication (= 2.9.8) fabrication (= 2.9.8)
fakeweb (~> 1.3.0) fakeweb (~> 1.3.0)
fast_blank fast_blank
@ -439,6 +443,7 @@ DEPENDENCIES
memory_profiler memory_profiler
message_bus (= 2.0.0.beta.11) message_bus (= 2.0.0.beta.11)
mime-types mime-types
mini_racer
minitest minitest
mocha mocha
mock_redis mock_redis
@ -493,7 +498,6 @@ DEPENDENCIES
sinatra sinatra
spork-rails spork-rails
stackprof stackprof
therubyracer
thin thin
timecop timecop
uglifier uglifier

View file

@ -59,7 +59,7 @@ SCRIPT
begin begin
code = transpile(node.inner_html, node['version']) code = transpile(node.inner_html, node['version'])
node.replace("<script>#{code}</script>") node.replace("<script>#{code}</script>")
rescue Tilt::ES6ModuleTranspilerTemplate::JavaScriptError => ex rescue MiniRacer::RuntimeError => ex
node.replace("<script type='text/discourse-js-error'>#{ex.message}</script>") node.replace("<script type='text/discourse-js-error'>#{ex.message}</script>")
end end
end end

View file

@ -1,22 +1,9 @@
require 'execjs' require 'execjs'
require 'babel/transpiler' require 'babel/transpiler'
require 'mini_racer'
module Tilt module Tilt
class Console
def initialize(prefix=nil)
@prefix = prefix || ''
end
def log(msg)
Rails.logger.info("#{@prefix}#{msg}")
end
def error(msg)
Rails.logger.error("#{@prefix}#{msg}")
end
end
class ES6ModuleTranspilerTemplate < Tilt::Template class ES6ModuleTranspilerTemplate < Tilt::Template
self.default_mime_type = 'application/javascript' self.default_mime_type = 'application/javascript'
@ -30,10 +17,19 @@ module Tilt
def self.create_new_context def self.create_new_context
# timeout any eval that takes longer than 15 seconds # timeout any eval that takes longer than 15 seconds
ctx = V8::Context.new(timeout: 15000) ctx = MiniRacer::Context.new(timeout: 15000)
ctx.eval("var self = this; #{File.read(Babel::Transpiler.script_path)}") ctx.eval("var self = this; #{File.read(Babel::Transpiler.script_path)}")
ctx.eval("module = {}; exports = {};"); ctx.eval("module = {}; exports = {};");
ctx.load("#{Rails.root}/lib/es6_module_transpiler/support/es6-module-transpiler.js") ctx.load("#{Rails.root}/lib/es6_module_transpiler/support/es6-module-transpiler.js")
ctx.attach("rails.logger.info", proc{|err| Rails.logger.info(err.to_s)})
ctx.attach("rails.logger.error", proc{|err| Rails.logger.error(err.to_s)})
ctx.eval <<JS
console = {
prefix: "",
log: function(msg){ rails.logger.info(console.prefix + msg); },
error: function(msg){ rails.logger.error(console.prefix + msg); }
}
JS
ctx ctx
end end
@ -60,20 +56,10 @@ module Tilt
end end
def self.protect def self.protect
rval = nil
@mutex.synchronize do @mutex.synchronize do
begin yield
rval = yield
# This may seem a bit odd, but we don't want to leak out
# objects that require locks on the v8 vm, to get a backtrace
# you need a lock, if this happens in the wrong spot you can
# deadlock a process
rescue V8::Error => e
raise JavaScriptError.new(e.message, e.backtrace)
end end
end end
rval
end
def whitelisted?(path) def whitelisted?(path)
@ -98,7 +84,7 @@ module Tilt
def babel_transpile(source) def babel_transpile(source)
klass = self.class klass = self.class
klass.protect do klass.protect do
klass.v8['console'] = Console.new("BABEL: babel-eval: ") klass.v8.eval("console.prefix = 'BABEL: babel-eval: ';")
@output = klass.v8.eval(babel_source(source)) @output = klass.v8.eval(babel_source(source))
end end
end end
@ -108,7 +94,7 @@ module Tilt
klass = self.class klass = self.class
klass.protect do klass.protect do
klass.v8['console'] = Console.new("BABEL: #{scope.logical_path}: ") klass.v8.eval("console.prefix = 'BABEL: #{scope.logical_path}: ';")
@output = klass.v8.eval(generate_source(scope)) @output = klass.v8.eval(generate_source(scope))
end end

View file

@ -154,15 +154,25 @@ module JsLocaleHelper
result result
end end
def self.compile_message_format(locale, format) @mutex = Mutex.new
ctx = V8::Context.new def self.with_context
@mutex.synchronize do
yield @ctx ||= begin
ctx = MiniRacer::Context.new
ctx.load(Rails.root + 'lib/javascripts/messageformat.js') ctx.load(Rails.root + 'lib/javascripts/messageformat.js')
ctx
end
end
end
def self.compile_message_format(locale, format)
with_context do |ctx|
path = Rails.root + "lib/javascripts/locale/#{locale}.js" path = Rails.root + "lib/javascripts/locale/#{locale}.js"
ctx.load(path) if File.exists?(path) ctx.load(path) if File.exists?(path)
ctx.eval("mf = new MessageFormat('#{locale}');") ctx.eval("mf = new MessageFormat('#{locale}');")
ctx.eval("mf.precompile(mf.parse(#{format.inspect}))") ctx.eval("mf.precompile(mf.parse(#{format.inspect}))")
end
rescue V8::Error => e rescue MiniRacer::EvalError => e
message = "Invalid Format: " << e.message message = "Invalid Format: " << e.message
"function(){ return #{message.inspect};}" "function(){ return #{message.inspect};}"
end end

View file

@ -1,4 +1,4 @@
require 'v8' require 'mini_racer'
require 'nokogiri' require 'nokogiri'
require_dependency 'url_helper' require_dependency 'url_helper'
require_dependency 'excerpt_parser' require_dependency 'excerpt_parser'
@ -7,7 +7,9 @@ require_dependency 'discourse_tagging'
module PrettyText module PrettyText
class Helpers module Helpers
extend self
def t(key, opts) def t(key, opts)
key = "js." + key key = "js." + key
unless opts unless opts
@ -81,6 +83,7 @@ module PrettyText
nil nil
end end
end end
end end
@mutex = Mutex.new @mutex = Mutex.new
@ -92,9 +95,11 @@ module PrettyText
def self.create_new_context def self.create_new_context
# timeout any eval that takes longer than 15 seconds # timeout any eval that takes longer than 15 seconds
ctx = V8::Context.new(timeout: 15000) ctx = MiniRacer::Context.new(timeout: 15000)
ctx["helpers"] = Helpers.new Helpers.instance_methods.each do |method|
ctx.attach("helpers.#{method}", Helpers.method(method))
end
ctx_load(ctx, ctx_load(ctx,
"vendor/assets/javascripts/md5.js", "vendor/assets/javascripts/md5.js",
@ -198,6 +203,7 @@ module PrettyText
# we use the exact same markdown converter as the client # we use the exact same markdown converter as the client
# TODO: use the same extensions on both client and server (in particular the template for mentions) # TODO: use the same extensions on both client and server (in particular the template for mentions)
baked = nil baked = nil
text = text || ""
protect do protect do
context = v8 context = v8
@ -206,8 +212,9 @@ module PrettyText
context_opts = opts || {} context_opts = opts || {}
context_opts[:sanitize] = true unless context_opts[:sanitize] == false context_opts[:sanitize] = true unless context_opts[:sanitize] == false
context['opts'] = context_opts
context['raw'] = text context.eval("opts = #{context_opts.to_json};")
context.eval("raw = #{text.inspect};")
if Post.white_listed_image_classes.present? if Post.white_listed_image_classes.present?
Post.white_listed_image_classes.each do |klass| Post.white_listed_image_classes.each do |klass|
@ -258,8 +265,10 @@ module PrettyText
# leaving this here, cause it invokes v8, don't want to implement twice # leaving this here, cause it invokes v8, don't want to implement twice
def self.avatar_img(avatar_template, size) def self.avatar_img(avatar_template, size)
protect do protect do
v8['avatarTemplate'] = avatar_template v8.eval <<JS
v8['size'] = size avatarTemplate = #{avatar_template.inspect};
size = #{size.to_i};
JS
decorate_context(v8) decorate_context(v8)
v8.eval("Discourse.Utilities.avatarImg({ avatarTemplate: avatarTemplate, size: size });") v8.eval("Discourse.Utilities.avatarImg({ avatarTemplate: avatarTemplate, size: size });")
end end
@ -267,9 +276,8 @@ module PrettyText
def self.unescape_emoji(title) def self.unescape_emoji(title)
protect do protect do
v8["title"] = title
decorate_context(v8) decorate_context(v8)
v8.eval("Discourse.Emoji.unescape(title)") v8.eval("Discourse.Emoji.unescape(#{title.inspect})")
end end
end end
@ -413,15 +421,7 @@ module PrettyText
def self.protect def self.protect
rval = nil rval = nil
@mutex.synchronize do @mutex.synchronize do
begin
rval = yield rval = yield
# This may seem a bit odd, but we don't want to leak out
# objects that require locks on the v8 vm, to get a backtrace
# you need a lock, if this happens in the wrong spot you can
# deadlock a process
rescue V8::Error => e
raise JavaScriptError.new(e.message, e.backtrace)
end
end end
rval rval
end end

View file

@ -1,5 +1,6 @@
require 'rails_helper' require 'rails_helper'
require_dependency 'js_locale_helper' require_dependency 'js_locale_helper'
require 'mini_racer'
describe JsLocaleHelper do describe JsLocaleHelper do
@ -25,7 +26,7 @@ describe JsLocaleHelper do
end end
def setup_message_format(format) def setup_message_format(format)
@ctx = V8::Context.new @ctx = MiniRacer::Context.new
@ctx.eval('MessageFormat = {locale: {}};') @ctx.eval('MessageFormat = {locale: {}};')
@ctx.load(Rails.root + 'lib/javascripts/locale/en.js') @ctx.load(Rails.root + 'lib/javascripts/locale/en.js')
compiled = JsLocaleHelper.compile_message_format('en', format) compiled = JsLocaleHelper.compile_message_format('en', format)
@ -72,7 +73,7 @@ describe JsLocaleHelper do
end end
it 'handles message format special keys' do it 'handles message format special keys' do
ctx = V8::Context.new ctx = MiniRacer::Context.new
ctx.eval("I18n = {};") ctx.eval("I18n = {};")
JsLocaleHelper.set_translations 'en', { JsLocaleHelper.set_translations 'en', {
@ -149,7 +150,7 @@ describe JsLocaleHelper do
SiteSetting.default_locale = 'ru' SiteSetting.default_locale = 'ru'
I18n.locale = :uk I18n.locale = :uk
ctx = V8::Context.new ctx = MiniRacer::Context.new
ctx.eval('var window = this;') ctx.eval('var window = this;')
ctx.load(Rails.root + 'app/assets/javascripts/locales/i18n.js') ctx.load(Rails.root + 'app/assets/javascripts/locales/i18n.js')
ctx.eval(JsLocaleHelper.output_locale(I18n.locale)) ctx.eval(JsLocaleHelper.output_locale(I18n.locale))
@ -167,7 +168,7 @@ describe JsLocaleHelper do
LocaleSiteSetting.values.each do |locale| LocaleSiteSetting.values.each do |locale|
it "generates valid date helpers for #{locale[:value]} locale" do it "generates valid date helpers for #{locale[:value]} locale" do
js = JsLocaleHelper.output_locale(locale[:value]) js = JsLocaleHelper.output_locale(locale[:value])
ctx = V8::Context.new ctx = MiniRacer::Context.new
ctx.eval('var window = this;') ctx.eval('var window = this;')
ctx.load(Rails.root + 'app/assets/javascripts/locales/i18n.js') ctx.load(Rails.root + 'app/assets/javascripts/locales/i18n.js')
ctx.eval(js) ctx.eval(js)