diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a80c190..26fb819 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,7 @@ jobs: strategy: fail-fast: false matrix: - ruby: [ '3.0', '2.7', '2.6', '2.5', 'jruby', 'truffleruby' ] + ruby: [ '3.0', '2.7', '2.6', '2.5', 'jruby', 'truffleruby', 'truffleruby+graalvm' ] runs-on: ubuntu-latest steps: - name: Checkout @@ -18,12 +18,17 @@ jobs: uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} + - name: Update Rubygems run: gem update --system - name: Install bundler run: gem install bundler -v '2.2.16' - name: Install dependencies run: bundle install + + - name: Set TRUFFLERUBYOPT + run: echo "TRUFFLERUBYOPT=--jvm --polyglot" >> $GITHUB_ENV + if: startsWith(matrix.ruby, 'truffleruby+graalvm') - name: Run test run: rake - name: Install gem diff --git a/lib/execjs/encoding.rb b/lib/execjs/encoding.rb index 406fb0d..50ef928 100644 --- a/lib/execjs/encoding.rb +++ b/lib/execjs/encoding.rb @@ -19,7 +19,7 @@ module ExecJS end else def encode(string) - string.encode('UTF-8') + string.encode(::Encoding::UTF_8) end end end diff --git a/lib/execjs/graaljs_runtime.rb b/lib/execjs/graaljs_runtime.rb new file mode 100644 index 0000000..73d22a7 --- /dev/null +++ b/lib/execjs/graaljs_runtime.rb @@ -0,0 +1,142 @@ +require "execjs/runtime" + +module ExecJS + class GraalJSRuntime < Runtime + class Context < Runtime::Context + def initialize(runtime, source = "", options = {}) + @context = Polyglot::InnerContext.new + @context.eval('js', 'delete this.console') + @js_object = @context.eval('js', 'Object') + + source = encode(source) + unless source.empty? + translate do + eval_in_context(source) + end + end + end + + def exec(source, options = {}) + source = encode(source) + source = "(function(){#{source}})()" if /\S/.match?(source) + + translate do + eval_in_context(source) + end + end + + def eval(source, options = {}) + source = encode(source) + source = "(#{source})" if /\S/.match?(source) + + translate do + eval_in_context(source) + end + end + + def call(source, *args) + source = encode(source) + source = "(#{source})" if /\S/.match?(source) + + translate do + function = eval_in_context(source) + function.call(*convert_ruby_to_js(args)) + end + end + + private + + def translate + convert_js_to_ruby yield + rescue ::RuntimeError => e + if e.message.start_with?('SyntaxError:') + error_class = ExecJS::RuntimeError + else + error_class = ExecJS::ProgramError + end + + backtrace = e.backtrace.map { |line| line.sub('(eval)', '(execjs)') } + raise error_class, e.message, backtrace + end + + def convert_js_to_ruby(value) + case value + when true, false, Integer, Float + value + else + if value.nil? + nil + elsif value.respond_to?(:call) + nil + elsif value.respond_to?(:to_str) + value.to_str + elsif value.respond_to?(:to_ary) + value.to_ary.map do |e| + if e.respond_to?(:call) + nil + else + convert_js_to_ruby(e) + end + end + else + object = value + h = {} + object.instance_variables.each do |member| + v = object[member] + unless v.respond_to?(:call) + h[member.to_s] = convert_js_to_ruby(v) + end + end + h + end + end + end + + def convert_ruby_to_js(value) + case value + when nil, true, false, Integer, Float, String + value + when Array + value.map { |e| convert_ruby_to_js(e) } + when Hash + h = @js_object.new + value.each_pair do |k,v| + h[convert_ruby_to_js(k)] = convert_ruby_to_js(v) + end + h + else + raise TypeError, "Unknown how to convert to JS: #{value.inspect}" + end + end + + class_eval <<-'RUBY', "(execjs)", 1 + def eval_in_context(code); @context.eval('js', code); end + RUBY + end + + def name + "GraalVM (Graal.js)" + end + + def available? + return @available if defined?(@available) + + unless RUBY_ENGINE == "truffleruby" + return @available = false + end + + unless defined?(Polyglot::InnerContext) + warn "TruffleRuby #{RUBY_ENGINE_VERSION} does not have support for inner contexts, use a more recent version", uplevel: 0 if $VERBOSE + return @available = false + end + + unless Polyglot.languages.include? "js" + warn "The language 'js' is not available, you likely need to `export TRUFFLERUBYOPT='--jvm --polyglot'`", uplevel: 0 if $VERBOSE + warn "Note that you need TruffleRuby+GraalVM and not just the TruffleRuby standalone to use #{self.class}", uplevel: 0 if $VERBOSE + return @available = false + end + + @available = true + end + end +end diff --git a/lib/execjs/runtimes.rb b/lib/execjs/runtimes.rb index 19d9d96..615ecd6 100644 --- a/lib/execjs/runtimes.rb +++ b/lib/execjs/runtimes.rb @@ -4,6 +4,7 @@ require "execjs/duktape_runtime" require "execjs/external_runtime" require "execjs/ruby_rhino_runtime" require "execjs/mini_racer_runtime" +require "execjs/graaljs_runtime" module ExecJS module Runtimes @@ -13,6 +14,8 @@ module ExecJS RubyRhino = RubyRhinoRuntime.new + GraalJS = GraalJSRuntime.new + MiniRacer = MiniRacerRuntime.new Node = ExternalRuntime.new( @@ -82,6 +85,7 @@ module ExecJS def self.runtimes @runtimes ||= [ RubyRhino, + GraalJS, Duktape, MiniRacer, Node, diff --git a/lib/execjs/support/jsc_runner.js b/lib/execjs/support/jsc_runner.js index 902ad4d..631aa46 100644 --- a/lib/execjs/support/jsc_runner.js +++ b/lib/execjs/support/jsc_runner.js @@ -2,7 +2,6 @@ }, function(program) { var output; try { - delete this.console; delete this.console; delete this.setTimeout; delete this.setInterval; diff --git a/test/test_execjs.rb b/test/test_execjs.rb index 99d8f0a..a860049 100644 --- a/test/test_execjs.rb +++ b/test/test_execjs.rb @@ -156,7 +156,7 @@ class TestExecJS < Test assert_output value, ExecJS.eval("#{json_value}") end - define_method("test_strinigfy_value_#{index}") do + define_method("test_stringify_value_#{index}") do context = ExecJS.compile("function json(obj) { return JSON.stringify(obj); }") assert_output json_value, context.call("json", value) end