js 0.0.1 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/js.gemspec CHANGED
@@ -1,19 +1,29 @@
1
- # -*- encoding: utf-8 -*-
2
- lib = File.expand_path('../lib', __FILE__)
3
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require 'js/version'
1
+ # frozen_string_literal: true
5
2
 
6
- Gem::Specification.new do |gem|
7
- gem.name = "js"
8
- gem.version = Js::VERSION
9
- gem.authors = ["Charles Lowell"]
10
- gem.email = ["cowboyd@thefrontside.net"]
11
- gem.description = %q{embed a JavaScript interpreter}
12
- gem.summary = %q{use JavaScript objects from Ruby and embed Ruby objects into JavaScript}
13
- gem.homepage = ""
3
+ require_relative "lib/js/version"
14
4
 
15
- gem.files = `git ls-files`.split($/)
16
- gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
- gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
- gem.require_paths = ["lib"]
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "js"
7
+ spec.version = JS::VERSION
8
+ spec.authors = ["Yuta Saito"]
9
+ spec.email = ["kateinoigakukun@gmail.com"]
10
+
11
+ spec.summary = %q{JavaScript bindings for ruby.wasm}
12
+ spec.description = %q{JavaScript bindings for ruby.wasm. This gem provides a way to use JavaScript functionalities from Ruby through WebAssembly.}
13
+ spec.homepage = "https://github.com/ruby/ruby.wasm"
14
+
15
+ spec.metadata = {
16
+ "source_code_uri" => "https://github.com/ruby/ruby.wasm/tree/main/packages/gems/js",
17
+ }
18
+
19
+ spec.license = "MIT"
20
+
21
+ spec.files = Dir.chdir(__dir__) do
22
+ `git ls-files -z`.split("\x0").reject do |f|
23
+ (File.expand_path(f) == __FILE__) ||
24
+ f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile])
25
+ end
26
+ end
27
+ spec.require_paths = ["lib"]
28
+ spec.extensions = ["ext/js/extconf.rb", "ext/witapi/extconf.rb"]
19
29
  end
data/lib/js/array.rb ADDED
@@ -0,0 +1,9 @@
1
+ class Array
2
+ # Convert Ruby array to JavaScript array
3
+ def to_js
4
+ new_array = JS.eval("return []")
5
+ # NOTE: This method call implicitly convert element to JS object by to_js
6
+ new_array.push *self
7
+ new_array
8
+ end
9
+ end
data/lib/js/hash.rb ADDED
@@ -0,0 +1,8 @@
1
+ class Hash
2
+ # Convert a hash to a JavaScript object
3
+ def to_js
4
+ new_object = JS.eval("return {}")
5
+ self.each { |key, value| new_object[key] = value }
6
+ new_object
7
+ end
8
+ end
@@ -0,0 +1,6 @@
1
+ class NilClass
2
+ # Convert to JS null
3
+ def to_js
4
+ JS::Null
5
+ end
6
+ end
@@ -0,0 +1,15 @@
1
+ module JS
2
+ class RequireRemote
3
+ # Execute the body of the response and record the URL.
4
+ class Evaluator
5
+ def evaluate(code, filename, final_url)
6
+ Kernel.eval(code, ::Object::TOPLEVEL_BINDING, filename)
7
+ $LOADED_FEATURES << final_url
8
+ end
9
+
10
+ def evaluated?(url)
11
+ $LOADED_FEATURES.include?(url)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,45 @@
1
+ module JS
2
+ class RequireRemote
3
+ ScriptLocation = Data.define(:url, :filename)
4
+
5
+ # When require_relative is called within a running Ruby script,
6
+ # the URL is resolved from a relative file path based on the URL of the running Ruby script.
7
+ # It uses a stack to store URLs of running Ruby Script.
8
+ # Push the URL onto the stack before executing the new script.
9
+ # Then pop it when the script has finished executing.
10
+ class URLResolver
11
+ def initialize(base_url)
12
+ @url_stack = [base_url]
13
+ end
14
+
15
+ def get_location(relative_feature)
16
+ filename = filename_from(relative_feature)
17
+ url = resolve(filename)
18
+ ScriptLocation.new(url, filename)
19
+ end
20
+
21
+ def push(url)
22
+ @url_stack.push url
23
+ end
24
+
25
+ def pop()
26
+ @url_stack.pop
27
+ end
28
+
29
+ private
30
+
31
+ def filename_from(relative_feature)
32
+ if relative_feature.end_with?(".rb")
33
+ relative_feature
34
+ else
35
+ "#{relative_feature}.rb"
36
+ end
37
+ end
38
+
39
+ # Return a URL object of JavaScript.
40
+ def resolve(relative_filepath)
41
+ JS.global[:URL].new relative_filepath, @url_stack.last
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,85 @@
1
+ require "singleton"
2
+ require "js"
3
+ require_relative "./require_remote/url_resolver"
4
+ require_relative "./require_remote/evaluator"
5
+
6
+ module JS
7
+ # This class is used to load remote Ruby scripts.
8
+ #
9
+ # == Example
10
+ #
11
+ # require 'js/require_remote'
12
+ # JS::RequireRemote.instance.load("foo")
13
+ #
14
+ # This class is intended to be used to replace Kernel#require_relative.
15
+ #
16
+ # == Example
17
+ #
18
+ # require 'js/require_remote'
19
+ # module Kernel
20
+ # def require_relative(path) = JS::RequireRemote.instance.load(path)
21
+ # end
22
+ #
23
+ # If you want to load the bundled gem
24
+ #
25
+ # == Example
26
+ #
27
+ # require 'js/require_remote'
28
+ # module Kernel
29
+ # alias original_require_relative require_relative
30
+ #
31
+ # def require_relative(path)
32
+ # caller_path = caller_locations(1, 1).first.absolute_path || ''
33
+ # dir = File.dirname(caller_path)
34
+ # file = File.absolute_path(path, dir)
35
+ #
36
+ # original_require_relative(file)
37
+ # rescue LoadError
38
+ # JS::RequireRemote.instance.load(path)
39
+ # end
40
+ # end
41
+ #
42
+ class RequireRemote
43
+ include Singleton
44
+
45
+ def initialize
46
+ base_url = JS.global[:URL].new(JS.global[:location][:href])
47
+ @resolver = URLResolver.new(base_url)
48
+ @evaluator = Evaluator.new
49
+ end
50
+
51
+ # Load the given feature from remote.
52
+ def load(relative_feature)
53
+ location = @resolver.get_location(relative_feature)
54
+
55
+ # Do not load the same URL twice.
56
+ return false if @evaluator.evaluated?(location.url[:href].to_s)
57
+
58
+ response = JS.global.fetch(location.url).await
59
+ unless response[:status].to_i == 200
60
+ raise LoadError.new "cannot load such url -- #{response[:status]} #{location.url}"
61
+ end
62
+
63
+ # The fetch API may have responded to a redirect response
64
+ # and fetched the script from a different URL than the original URL.
65
+ # Retrieve the final URL again from the response object.
66
+ final_url = response[:url].to_s
67
+
68
+ # Do not evaluate the same URL twice.
69
+ return false if @evaluator.evaluated?(final_url)
70
+
71
+ code = response.text().await.to_s
72
+
73
+ evaluate(code, location.filename, final_url)
74
+ end
75
+
76
+ private
77
+
78
+ def evaluate(code, filename, final_url)
79
+ @resolver.push(final_url)
80
+ @evaluator.evaluate(code, filename, final_url)
81
+ @resolver.pop
82
+ true
83
+ end
84
+ end
85
+ end
data/lib/js/version.rb CHANGED
@@ -1,3 +1,3 @@
1
- module Js
2
- VERSION = "0.0.1"
1
+ module JS
2
+ VERSION = "2.5.0"
3
3
  end
data/lib/js.rb CHANGED
@@ -1,5 +1,240 @@
1
- require "js/version"
1
+ require "js.so"
2
+ require_relative "js/hash.rb"
3
+ require_relative "js/array.rb"
4
+ require_relative "js/nil_class.rb"
2
5
 
3
- module Js
4
- # Your code goes here...
6
+ # The JS module provides a way to interact with JavaScript from Ruby.
7
+ #
8
+ # == Example
9
+ #
10
+ # require 'js'
11
+ # JS.eval("return 1 + 2") # => 3
12
+ # JS.global[:document].write("Hello, world!")
13
+ # div = JS.global[:document].createElement("div")
14
+ # div[:innerText] = "click me"
15
+ # body = JS.global[:document][:body]
16
+ # if body[:classList].contains?("main")
17
+ # body.appendChild(div)
18
+ # end
19
+ # div.addEventListener("click") do |event|
20
+ # puts event # => # [object MouseEvent]
21
+ # puts event[:detail] # => 1
22
+ # div[:innerText] = "clicked!"
23
+ # end
24
+ #
25
+ # If you are using `ruby.wasm` without `stdlib` you will not have `addEventListener`
26
+ # and other specialized functions defined. You can still acomplish many
27
+ # of the same things using `call` instead.
28
+ #
29
+ # == Example
30
+ #
31
+ # require 'js'
32
+ # JS.eval("return 1 + 2") # => 3
33
+ # JS.global[:document].call(:write, "Hello, world!")
34
+ # div = JS.global[:document].call(:createElement, "div")
35
+ # div[:innerText] = "click me"
36
+ # if body[:classList].call(:contains, "main") == JS::True
37
+ # body.appendChild(div)
38
+ # end
39
+ # div.call(:addEventListener, "click") do |event|
40
+ # puts event # => # [object MouseEvent]
41
+ # puts event[:detail] # => 1
42
+ # div[:innerText] = "clicked!"
43
+ # end
44
+ #
45
+ module JS
46
+ Undefined = JS.eval("return undefined")
47
+ Null = JS.eval("return null")
48
+
49
+ # A boolean value in JavaScript is always a JS::Object instance from Ruby's point of view.
50
+ # If we use the boolean value returned by a JavaScript function as the condition for an if expression in Ruby,
51
+ # the if expression will always be true.
52
+ #
53
+ # == Bad Example
54
+ #
55
+ # searchParams = JS.global[:URLSearchParams].new(JS.global[:location][:search])
56
+ # if searchParams.has('phrase')
57
+ # # Always pass through here.
58
+ # ...
59
+ # else
60
+ # ...
61
+ # end
62
+ #
63
+ # Therefore, the JS::True constant is used to determine if the JavaScript function return value is true or false.
64
+ #
65
+ # == Good Example
66
+ #
67
+ # if searchParams.has('phrase') == JS::True
68
+ # ...
69
+ # end
70
+ True = JS.eval("return true;")
71
+ False = JS.eval("return false;")
72
+
73
+ class PromiseScheduler
74
+ def initialize(loop)
75
+ @loop = loop
76
+ end
77
+
78
+ def await(promise)
79
+ current = Fiber.current
80
+ promise.call(
81
+ :then,
82
+ ->(value) { current.transfer(value, :success) },
83
+ ->(value) { current.transfer(value, :failure) }
84
+ )
85
+ if @loop == current
86
+ raise (
87
+ "JS::Object#await can be called only from RubyVM#evalAsync or RbValue#callAsync JS API\n" +
88
+ "If you are using browser.script.iife.js, please ensure that you specify `data-eval=\"async\"` in your script tag\n" +
89
+ "e.g. <script type=\"text/ruby\" data-eval=\"async\">puts :hello</script>\n" +
90
+ "Or <script type=\"text/ruby\" data-eval=\"async\" src=\"path/to/script.rb\"></script>"
91
+ )
92
+ end
93
+ value, status = @loop.transfer
94
+ raise JS::Error.new(value) if status == :failure
95
+ value
96
+ end
97
+ end
98
+
99
+ @promise_scheduler = PromiseScheduler.new Fiber.current
100
+
101
+ def self.promise_scheduler
102
+ @promise_scheduler
103
+ end
104
+
105
+ private
106
+
107
+ def self.__eval_async_rb(rb_code, future)
108
+ self.__async(future) do
109
+ JS::Object.wrap(Kernel.eval(rb_code.to_s, TOPLEVEL_BINDING, "eval_async"))
110
+ end
111
+ end
112
+
113
+ def self.__call_async_method(recv, method_name, future, *args)
114
+ self.__async(future) { recv.send(method_name.to_s, *args) }
115
+ end
116
+
117
+ def self.__async(future, &block)
118
+ Fiber
119
+ .new do
120
+ future.resolve block.call
121
+ rescue => e
122
+ future.reject JS::Object.wrap(e)
123
+ end
124
+ .transfer
125
+ end
126
+ end
127
+
128
+ class JS::Object
129
+ # Create a JavaScript object with the new method
130
+ #
131
+ # The below examples show typical usage in Ruby
132
+ #
133
+ # JS.global[:Object].new
134
+ # JS.global[:Number].new(1.23)
135
+ # JS.global[:String].new("string")
136
+ # JS.global[:Array].new(1, 2, 3)
137
+ # JS.global[:Date].new(2020, 1, 1)
138
+ # JS.global[:Error].new("error message")
139
+ # JS.global[:URLSearchParams].new(JS.global[:location][:search])
140
+ #
141
+ def new(*args)
142
+ JS.global[:Reflect].construct(self, args.to_js)
143
+ end
144
+
145
+ # Converts +self+ to an Array:
146
+ #
147
+ # JS.eval("return [1, 2, 3]").to_a.map(&:to_i) # => [1, 2, 3]
148
+ # JS.global[:document].querySelectorAll("p").to_a # => [[object HTMLParagraphElement], ...
149
+ def to_a
150
+ as_array = JS.global[:Array].from(self)
151
+ Array.new(as_array[:length].to_i) { as_array[_1] }
152
+ end
153
+
154
+ # Provide a shorthand form for JS::Object#call
155
+ #
156
+ # This method basically calls the JavaScript method with the same
157
+ # name as the Ruby method name as is using JS::Object#call.
158
+ #
159
+ # Exceptions are the following cases:
160
+ # * If the method name ends with a question mark (?), the question mark is removed
161
+ # and the method is called as a predicate method. The return value is converted to
162
+ # a Ruby boolean value automatically.
163
+ #
164
+ # This shorthand is unavailable for the following cases and you need to use
165
+ # JS::Object#call instead:
166
+ # * If the method name is invalid as a Ruby method name (e.g. contains a hyphen, reserved word, etc.)
167
+ # * If the method name is already defined as a Ruby method under JS::Object
168
+ # * If the JavaScript method name ends with a question mark (?)
169
+ def method_missing(sym, *args, &block)
170
+ sym_str = sym.to_s
171
+ if sym_str.end_with?("?")
172
+ # When a JS method is called with a ? suffix, it is treated as a predicate method,
173
+ # and the return value is converted to a Ruby boolean value automatically.
174
+ self.call(sym_str[0..-2].to_sym, *args, &block) == JS::True
175
+ elsif self[sym].typeof == "function"
176
+ self.call(sym, *args, &block)
177
+ else
178
+ super
179
+ end
180
+ end
181
+
182
+ # Check if a JavaScript method exists
183
+ #
184
+ # See JS::Object#method_missing for details.
185
+ def respond_to_missing?(sym, include_private)
186
+ return true if super
187
+ sym_str = sym.to_s
188
+ sym = sym_str[0..-2].to_sym if sym_str.end_with?("?")
189
+ self[sym].typeof == "function"
190
+ end
191
+
192
+ # Await a JavaScript Promise like `await` in JavaScript.
193
+ # This method looks like a synchronous method, but it actually runs asynchronously using fibers.
194
+ # In other words, the next line to the `await` call at Ruby source will be executed after the
195
+ # promise will be resolved. However, it does not block JavaScript event loop, so the next line
196
+ # to the RubyVM.evalAsync` (in the case when no `await` operator before the call expression)
197
+ # at JavaScript source will be executed without waiting for the promise.
198
+ #
199
+ # The below example shows how the execution order goes. It goes in the order of "step N"
200
+ #
201
+ # # In JavaScript
202
+ # const response = vm.evalAsync(`
203
+ # puts "step 1"
204
+ # JS.global.fetch("https://example.com").await
205
+ # puts "step 3"
206
+ # `) // => Promise
207
+ # console.log("step 2")
208
+ # await response
209
+ # console.log("step 4")
210
+ #
211
+ # The below examples show typical usage in Ruby
212
+ #
213
+ # JS.eval("return new Promise((ok) => setTimeout(() => ok(42), 1000))").await # => 42 (after 1 second)
214
+ # JS.global.fetch("https://example.com").await # => [object Response]
215
+ # JS.eval("return 42").await # => 42
216
+ # JS.eval("return new Promise((ok, err) => err(new Error())").await # => raises JS::Error
217
+ def await
218
+ # Promise.resolve wrap a value or flattens promise-like object and its thenable chain
219
+ promise = JS.global[:Promise].resolve(self)
220
+ JS.promise_scheduler.await(promise)
221
+ end
222
+ end
223
+
224
+ # A wrapper class for JavaScript Error to allow the Error to be thrown in Ruby.
225
+ class JS::Error
226
+ def initialize(exception)
227
+ @exception = exception
228
+ super
229
+ end
230
+
231
+ def message
232
+ stack = @exception[:stack]
233
+ if stack.typeof == "string"
234
+ # Error.stack contains the error message also
235
+ stack.to_s
236
+ else
237
+ @exception.to_s
238
+ end
239
+ end
5
240
  end
metadata CHANGED
@@ -1,53 +1,70 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: js
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
5
- prerelease:
4
+ version: 2.5.0
6
5
  platform: ruby
7
6
  authors:
8
- - Charles Lowell
7
+ - Yuta Saito
9
8
  autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2012-12-18 00:00:00.000000000 Z
11
+ date: 2024-01-28 00:00:00.000000000 Z
13
12
  dependencies: []
14
- description: embed a JavaScript interpreter
13
+ description: JavaScript bindings for ruby.wasm. This gem provides a way to use JavaScript
14
+ functionalities from Ruby through WebAssembly.
15
15
  email:
16
- - cowboyd@thefrontside.net
16
+ - kateinoigakukun@gmail.com
17
17
  executables: []
18
- extensions: []
18
+ extensions:
19
+ - ext/js/extconf.rb
20
+ - ext/witapi/extconf.rb
19
21
  extra_rdoc_files: []
20
22
  files:
21
- - .gitignore
22
- - Gemfile
23
- - LICENSE.txt
24
- - README.md
25
- - Rakefile
23
+ - ext/js/bindgen/.clang-format
24
+ - ext/js/bindgen/rb-js-abi-host.c
25
+ - ext/js/bindgen/rb-js-abi-host.h
26
+ - ext/js/bindgen/rb-js-abi-host.wit
27
+ - ext/js/depend
28
+ - ext/js/extconf.rb
29
+ - ext/js/js-core.c
30
+ - ext/witapi/bindgen/.clang-format
31
+ - ext/witapi/bindgen/rb-abi-guest.c
32
+ - ext/witapi/bindgen/rb-abi-guest.h
33
+ - ext/witapi/bindgen/rb-abi-guest.wit
34
+ - ext/witapi/depend
35
+ - ext/witapi/extconf.rb
36
+ - ext/witapi/witapi-core.c
26
37
  - js.gemspec
27
38
  - lib/js.rb
39
+ - lib/js/array.rb
40
+ - lib/js/hash.rb
41
+ - lib/js/nil_class.rb
42
+ - lib/js/require_remote.rb
43
+ - lib/js/require_remote/evaluator.rb
44
+ - lib/js/require_remote/url_resolver.rb
28
45
  - lib/js/version.rb
29
- homepage: ''
30
- licenses: []
46
+ homepage: https://github.com/ruby/ruby.wasm
47
+ licenses:
48
+ - MIT
49
+ metadata:
50
+ source_code_uri: https://github.com/ruby/ruby.wasm/tree/main/packages/gems/js
31
51
  post_install_message:
32
52
  rdoc_options: []
33
53
  require_paths:
34
54
  - lib
35
55
  required_ruby_version: !ruby/object:Gem::Requirement
36
- none: false
37
56
  requirements:
38
- - - ! '>='
57
+ - - ">="
39
58
  - !ruby/object:Gem::Version
40
59
  version: '0'
41
60
  required_rubygems_version: !ruby/object:Gem::Requirement
42
- none: false
43
61
  requirements:
44
- - - ! '>='
62
+ - - ">="
45
63
  - !ruby/object:Gem::Version
46
64
  version: '0'
47
65
  requirements: []
48
- rubyforge_project:
49
- rubygems_version: 1.8.24
66
+ rubygems_version: 3.5.3
50
67
  signing_key:
51
- specification_version: 3
52
- summary: use JavaScript objects from Ruby and embed Ruby objects into JavaScript
68
+ specification_version: 4
69
+ summary: JavaScript bindings for ruby.wasm
53
70
  test_files: []
data/.gitignore DELETED
@@ -1,17 +0,0 @@
1
- *.gem
2
- *.rbc
3
- .bundle
4
- .config
5
- .yardoc
6
- Gemfile.lock
7
- InstalledFiles
8
- _yardoc
9
- coverage
10
- doc/
11
- lib/bundler/man
12
- pkg
13
- rdoc
14
- spec/reports
15
- test/tmp
16
- test/version_tmp
17
- tmp
data/Gemfile DELETED
@@ -1,4 +0,0 @@
1
- source 'https://rubygems.org'
2
-
3
- # Specify your gem's dependencies in js.gemspec
4
- gemspec
data/LICENSE.txt DELETED
@@ -1,22 +0,0 @@
1
- Copyright (c) 2012 Charles Lowell
2
-
3
- MIT License
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining
6
- a copy of this software and associated documentation files (the
7
- "Software"), to deal in the Software without restriction, including
8
- without limitation the rights to use, copy, modify, merge, publish,
9
- distribute, sublicense, and/or sell copies of the Software, and to
10
- permit persons to whom the Software is furnished to do so, subject to
11
- the following conditions:
12
-
13
- The above copyright notice and this permission notice shall be
14
- included in all copies or substantial portions of the Software.
15
-
16
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
- LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md DELETED
@@ -1,35 +0,0 @@
1
- # js.rb - Embed JavasScript into Ruby
2
-
3
- js.rb harnesses the power of JavaScript inside your Ruby application
4
-
5
- ## Synopsis
6
-
7
- evaluate some simple JavaScript
8
-
9
- cxt = JS::Context.new
10
- cxt.eval('7 * 6') #=> 42
11
-
12
- embed values into the scope of your context
13
-
14
- cxt['foo'] = "bar"
15
- cxt.eval('foo') # => "bar"
16
-
17
- embed Ruby code into your scope and call it from JavaScript
18
-
19
- cxt["say"] = lambda {|this, word, times| word * times}
20
- cxt.eval("say('Hello', 3)") #=> HelloHelloHello
21
-
22
- ## Installation
23
-
24
- Add this line to your application's Gemfile:
25
-
26
- gem 'js'
27
-
28
- And then execute:
29
-
30
- $ bundle
31
-
32
- Or install it yourself as:
33
-
34
- $ gem install js
35
-
data/Rakefile DELETED
@@ -1 +0,0 @@
1
- require "bundler/gem_tasks"