js 0.0.1 → 2.5.0

Sign up to get free protection for your applications and to get access to all the features.
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"