From 253e5d670ddb7d57a74cd50e9d4bb42d9df060ce Mon Sep 17 00:00:00 2001 From: ledsun Date: Mon, 19 Aug 2024 08:40:38 +0900 Subject: [PATCH 1/7] JS::Object inherits BasicObject To enable calling the JavaScript send method. --- packages/gems/js/ext/js/js-core.c | 2 +- packages/gems/js/lib/js.rb | 44 ++++++++++++++----- .../ruby-wasm-wasi/test/unit/test_object.rb | 26 +++++++++-- 3 files changed, 56 insertions(+), 16 deletions(-) diff --git a/packages/gems/js/ext/js/js-core.c b/packages/gems/js/ext/js/js-core.c index d42b47758d..f8cd68badc 100644 --- a/packages/gems/js/ext/js/js-core.c +++ b/packages/gems/js/ext/js/js-core.c @@ -580,7 +580,7 @@ void Init_js() { rb_define_module_function(rb_mJS, "global", _rb_js_global_this, 0); i_to_js = rb_intern("to_js"); - rb_cJS_Object = rb_define_class_under(rb_mJS, "Object", rb_cObject); + rb_cJS_Object = rb_define_class_under(rb_mJS, "Object", rb_cBasicObject); VALUE rb_cJS_singleton = rb_singleton_class(rb_cJS_Object); rb_define_alloc_func(rb_cJS_Object, jsvalue_s_allocate); rb_define_method(rb_cJS_Object, "[]", _rb_js_obj_aref, 1); diff --git a/packages/gems/js/lib/js.rb b/packages/gems/js/lib/js.rb index cd73649bb4..5feffd666f 100644 --- a/packages/gems/js/lib/js.rb +++ b/packages/gems/js/lib/js.rb @@ -125,7 +125,23 @@ def self.__async(future, &block) end end -class JS::Object +# Inherit BasicObject to prevent define coventional menthods. #Override the `Object#send` to give priority to `send` method of JavaScript. +# +# This is to make it easier to use JavaScript Objects with `send` method such as `WebSocket` and `XMLHttpRequest`. +# The JavaScript method call short-hand in `JS::Object` is implemented using `method_missing`. +# If JS::Object inherits from Object, the `send` method defined in Ruby will take precedence over the JavaScript `send` method. +# If you want to call the JavaScript `send` method, you must use the `call` method as follows: +# +# ws = JS.global[:WebSocket].new("ws://example.com") +# ws.call(:send, ["Hello, world! from Ruby"]) +# +# This inheritation allows you to call the JavaScript `send` method with the following syntax: +# +# ws.send("Hello, world! from Ruby") + + + +class JS::Object < BasicObject # Create a JavaScript object with the new method # # The below examples show typical usage in Ruby @@ -141,7 +157,7 @@ class JS::Object # def new(*args, &block) args = args + [block] if block - JS.global[:Reflect].construct(self, args.to_js) + ::JS.global[:Reflect].construct(self, args.to_js) end # Converts +self+ to an Array: @@ -149,8 +165,8 @@ def new(*args, &block) # JS.eval("return [1, 2, 3]").to_a.map(&:to_i) # => [1, 2, 3] # JS.global[:document].querySelectorAll("p").to_a # => [[object HTMLParagraphElement], ... def to_a - as_array = JS.global[:Array].from(self) - Array.new(as_array[:length].to_i) { as_array[_1] } + as_array = ::JS.global[:Array].from(self) + ::Array.new(as_array[:length].to_i) { as_array[_1] } end # Provide a shorthand form for JS::Object#call @@ -176,7 +192,7 @@ def method_missing(sym, *args, &block) result = invoke_js_method(sym_str[0..-2].to_sym, *args, &block) # Type coerce the result to boolean type # to match the true/false determination in JavaScript's if statement. - return JS.global.Boolean(result) == JS::True + return ::JS.global.Boolean(result) == ::JS::True end invoke_js_method(sym, *args, &block) @@ -186,7 +202,6 @@ def method_missing(sym, *args, &block) # # See JS::Object#method_missing for details. def respond_to_missing?(sym, include_private) - return true if super sym_str = sym.to_s sym = sym_str[0..-2].to_sym if sym_str.end_with?("?") self[sym].typeof == "function" @@ -203,7 +218,7 @@ def respond_to_missing?(sym, include_private) # end.await # => 42 def apply(*args, &block) args = args + [block] if block - JS.global[:Reflect].call(:apply, self, JS::Undefined, args.to_js) + ::JS.global[:Reflect].call(:apply, self, ::JS::Undefined, args.to_js) end # Await a JavaScript Promise like `await` in JavaScript. @@ -233,8 +248,13 @@ def apply(*args, &block) # JS.eval("return new Promise((ok, err) => err(new Error())").await # => raises JS::Error def await # Promise.resolve wrap a value or flattens promise-like object and its thenable chain - promise = JS.global[:Promise].resolve(self) - JS.promise_scheduler.await(promise) + promise = ::JS.global[:Promise].resolve(self) + ::JS.promise_scheduler.await(promise) + end + + # https://github.com/rails/rails/blob/5c0b7496ab32c25c80f6d1bdc8b32ec6f75ce1e4/activerecord/lib/active_record/promise.rb#L40-L42 + [:nil?, :is_a?].each do |method| + define_method(method, ::Object.instance_method(method)) end private @@ -246,12 +266,12 @@ def invoke_js_method(sym, *args, &block) return self.call(sym, *args, &block) if self[sym].typeof == "function" # Check to see if a non-functional property exists. - if JS.global[:Reflect].call(:has, self, sym.to_s) == JS::True - raise TypeError, + if ::JS.global[:Reflect].call(:has, self, sym.to_s) == ::JS::True + raise ::TypeError, "`#{sym}` is not a function. To reference a property, use `[:#{sym}]` syntax instead." end - raise NoMethodError, + raise ::NoMethodError, "undefined method `#{sym}' for an instance of JS::Object" end end diff --git a/packages/npm-packages/ruby-wasm-wasi/test/unit/test_object.rb b/packages/npm-packages/ruby-wasm-wasi/test/unit/test_object.rb index a0af8241d7..2ac77b62cc 100644 --- a/packages/npm-packages/ruby-wasm-wasi/test/unit/test_object.rb +++ b/packages/npm-packages/ruby-wasm-wasi/test/unit/test_object.rb @@ -368,9 +368,29 @@ def test_respond_to_missing? object = JS.eval(<<~JS) return { foo() { return true; } }; JS - assert_true object.respond_to?(:foo) - assert_true object.respond_to?(:new) - assert_false object.respond_to?(:bar) + assert_true object.__send__(:respond_to_missing?, :foo, false) + assert_false object.__send__(:respond_to_missing?, :bar, false) + + # new is method of JS::Object + assert_false object.__send__(:respond_to_missing?, :new, false) + + # send is not implemented in JS::Object, + # because JS::Object is a subclass of JS::BaseObject + assert_false object.__send__(:respond_to_missing?, :send, false) + end + + def test_send_method_for_javascript_object_with_send_method + object = JS.eval(<<~JS) + return { send(message) { return message; } }; + JS + assert_equal "hello", object.send('hello').to_s + end + + def test_send_method_for_javascript_object_without_send_method + object = JS.eval(<<~JS) + return { write(message) { return message; } }; + JS + assert_raise(NoMethodError) { object.send('hello') } end def test_member_get From fcca29396ebdf5e68f4cbcdab5e4820b2a682164 Mon Sep 17 00:00:00 2001 From: ledsun Date: Sun, 20 Oct 2024 00:39:17 +0900 Subject: [PATCH 2/7] Define additional methods to JS::Object for testing --- packages/gems/js/lib/js.rb | 5 +++-- .../ruby-wasm-wasi/test/unit/test_error.rb | 2 ++ .../ruby-wasm-wasi/test/unit/test_float.rb | 2 ++ .../ruby-wasm-wasi/tools/run-test-unit.mjs | 12 ++++++++++++ 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/packages/gems/js/lib/js.rb b/packages/gems/js/lib/js.rb index 5feffd666f..b35977058a 100644 --- a/packages/gems/js/lib/js.rb +++ b/packages/gems/js/lib/js.rb @@ -252,8 +252,9 @@ def await ::JS.promise_scheduler.await(promise) end - # https://github.com/rails/rails/blob/5c0b7496ab32c25c80f6d1bdc8b32ec6f75ce1e4/activerecord/lib/active_record/promise.rb#L40-L42 - [:nil?, :is_a?].each do |method| + # I don't know why, but I can't define the respond_to? method in refinements. + # I'm defining it here instead. + [:nil?, :is_a?, :raise, :respond_to?].each do |method| define_method(method, ::Object.instance_method(method)) end diff --git a/packages/npm-packages/ruby-wasm-wasi/test/unit/test_error.rb b/packages/npm-packages/ruby-wasm-wasi/test/unit/test_error.rb index 1438a552a4..dfec73599e 100644 --- a/packages/npm-packages/ruby-wasm-wasi/test/unit/test_error.rb +++ b/packages/npm-packages/ruby-wasm-wasi/test/unit/test_error.rb @@ -2,6 +2,8 @@ require "js" class JS::TestError < Test::Unit::TestCase + using JsObjectTestable + def test_throw_error e = assert_raise(JS::Error) { JS.eval("throw new Error('foo')") } assert_match /^Error: foo/, e.message diff --git a/packages/npm-packages/ruby-wasm-wasi/test/unit/test_float.rb b/packages/npm-packages/ruby-wasm-wasi/test/unit/test_float.rb index 917bca56f4..4f6df081de 100644 --- a/packages/npm-packages/ruby-wasm-wasi/test/unit/test_float.rb +++ b/packages/npm-packages/ruby-wasm-wasi/test/unit/test_float.rb @@ -2,6 +2,8 @@ require "js" class JS::TestFloat < Test::Unit::TestCase + using JsObjectTestable + def test_to_js assert_equal (1.0).to_js, JS.eval("return 1.0;") assert_equal (0.5).to_js, JS.eval("return 0.5;") diff --git a/packages/npm-packages/ruby-wasm-wasi/tools/run-test-unit.mjs b/packages/npm-packages/ruby-wasm-wasi/tools/run-test-unit.mjs index 70d8ec06f6..6d851fc507 100755 --- a/packages/npm-packages/ruby-wasm-wasi/tools/run-test-unit.mjs +++ b/packages/npm-packages/ruby-wasm-wasi/tools/run-test-unit.mjs @@ -169,6 +169,18 @@ const test = async (instantiate) => { await vm.evalAsync(` require 'test/unit' + + # Define the methods to be used for unit testing assertions. + # Use refinements to limit the scope of influence. + require 'pp' + module JsObjectTestable + refine JS::Object do + [:object_id, :pretty_inspect].each do |method| + define_method(method, ::Object.instance_method(method)) + end + end + end + require_relative '${rootTestFile}' ok = Test::Unit::AutoRunner.run exit(1) unless ok From ec7346537b4304dac1c1d2992b7b34797b3ccadb Mon Sep 17 00:00:00 2001 From: ledsun Date: Sun, 27 Oct 2024 23:51:08 +0900 Subject: [PATCH 3/7] Fix comments for `respond_to?` method. --- packages/gems/js/lib/js.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/gems/js/lib/js.rb b/packages/gems/js/lib/js.rb index b35977058a..bbf7e4bc99 100644 --- a/packages/gems/js/lib/js.rb +++ b/packages/gems/js/lib/js.rb @@ -252,8 +252,11 @@ def await ::JS.promise_scheduler.await(promise) end - # I don't know why, but I can't define the respond_to? method in refinements. - # I'm defining it here instead. + # The `respond_to?` method is only used in unit tests. + # There is little need to define it here. + # However, methods suffixed with `?` do not conflict with JavaScript methods. + # As there are no disadvantages, we will define the `respond_to?` method here + # in the same way as the `nil?` and `is_a?` methods, prioritizing convenience. [:nil?, :is_a?, :raise, :respond_to?].each do |method| define_method(method, ::Object.instance_method(method)) end From 9e05673f590a78e65a5c48303693e659474417e8 Mon Sep 17 00:00:00 2001 From: ledsun Date: Sun, 27 Oct 2024 23:57:05 +0900 Subject: [PATCH 4/7] Fix comments about a workaround for the test-unit gem. --- packages/npm-packages/ruby-wasm-wasi/tools/run-test-unit.mjs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/npm-packages/ruby-wasm-wasi/tools/run-test-unit.mjs b/packages/npm-packages/ruby-wasm-wasi/tools/run-test-unit.mjs index 6d851fc507..7bc957620a 100755 --- a/packages/npm-packages/ruby-wasm-wasi/tools/run-test-unit.mjs +++ b/packages/npm-packages/ruby-wasm-wasi/tools/run-test-unit.mjs @@ -170,8 +170,9 @@ const test = async (instantiate) => { await vm.evalAsync(` require 'test/unit' - # Define the methods to be used for unit testing assertions. - # Use refinements to limit the scope of influence. + # FIXME: This is a workaround for the test-unit gem. + # It will be removed when the next pull request is merged and released. + # https://github.com/test-unit/test-unit/pull/262 require 'pp' module JsObjectTestable refine JS::Object do From a2aa724ac3411c1c7563ed56c8ed855634d79292 Mon Sep 17 00:00:00 2001 From: ledsun Date: Mon, 28 Oct 2024 00:00:18 +0900 Subject: [PATCH 5/7] Remove unneccesary blank lines. --- packages/gems/js/lib/js.rb | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/gems/js/lib/js.rb b/packages/gems/js/lib/js.rb index bbf7e4bc99..3564065f82 100644 --- a/packages/gems/js/lib/js.rb +++ b/packages/gems/js/lib/js.rb @@ -138,9 +138,6 @@ def self.__async(future, &block) # This inheritation allows you to call the JavaScript `send` method with the following syntax: # # ws.send("Hello, world! from Ruby") - - - class JS::Object < BasicObject # Create a JavaScript object with the new method # From 11850aa01576dad849d9e22af20bb7c4253ab2f4 Mon Sep 17 00:00:00 2001 From: ledsun Date: Mon, 28 Oct 2024 00:01:05 +0900 Subject: [PATCH 6/7] Fix indent --- .../npm-packages/ruby-wasm-wasi/tools/run-test-unit.mjs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/npm-packages/ruby-wasm-wasi/tools/run-test-unit.mjs b/packages/npm-packages/ruby-wasm-wasi/tools/run-test-unit.mjs index 7bc957620a..41c850a32d 100755 --- a/packages/npm-packages/ruby-wasm-wasi/tools/run-test-unit.mjs +++ b/packages/npm-packages/ruby-wasm-wasi/tools/run-test-unit.mjs @@ -175,11 +175,11 @@ const test = async (instantiate) => { # https://github.com/test-unit/test-unit/pull/262 require 'pp' module JsObjectTestable - refine JS::Object do - [:object_id, :pretty_inspect].each do |method| - define_method(method, ::Object.instance_method(method)) - end + refine JS::Object do + [:object_id, :pretty_inspect].each do |method| + define_method(method, ::Object.instance_method(method)) end + end end require_relative '${rootTestFile}' From 58e38536f2602539bca6204806dd2224d94b5a90 Mon Sep 17 00:00:00 2001 From: ledsun Date: Mon, 28 Oct 2024 00:05:05 +0900 Subject: [PATCH 7/7] rake format --- packages/npm-packages/ruby-wasm-wasi/test/unit/test_object.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/npm-packages/ruby-wasm-wasi/test/unit/test_object.rb b/packages/npm-packages/ruby-wasm-wasi/test/unit/test_object.rb index 2ac77b62cc..812bebe22b 100644 --- a/packages/npm-packages/ruby-wasm-wasi/test/unit/test_object.rb +++ b/packages/npm-packages/ruby-wasm-wasi/test/unit/test_object.rb @@ -383,14 +383,14 @@ def test_send_method_for_javascript_object_with_send_method object = JS.eval(<<~JS) return { send(message) { return message; } }; JS - assert_equal "hello", object.send('hello').to_s + assert_equal "hello", object.send("hello").to_s end def test_send_method_for_javascript_object_without_send_method object = JS.eval(<<~JS) return { write(message) { return message; } }; JS - assert_raise(NoMethodError) { object.send('hello') } + assert_raise(NoMethodError) { object.send("hello") } end def test_member_get