jsg 0.1.2 → 0.2.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +19 -0
- data/README.md +91 -15
- data/exe/jsg +3 -95
- data/lib/jsg/cli.rb +110 -0
- data/lib/jsg/version.rb +1 -1
- data/lib/jsg.rb +211 -0
- data/sig/jsg.rbs +1 -1
- data/template/Gemfile +6 -5
- data/template/assets/browser.script.iife.js +244 -145
- data/template/index.html.erb +6 -22
- data/template/src/main.rb +9 -2
- metadata +17 -26
- data/.idea/.gitignore +0 -8
- data/.idea/jsg.iml +0 -94
- data/.idea/misc.xml +0 -4
- data/.idea/modules.xml +0 -8
- data/.idea/vcs.xml +0 -6
- data/.rspec +0 -3
- data/.rubocop.yml +0 -8
- data/Rakefile +0 -12
- data/jsg.rb +0 -198
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f1d60a874b6a2f773a473b333c5f2ff4db6d47f532dc1a257c1c1c34b7a226f8
|
|
4
|
+
data.tar.gz: 5f777d0b59c0454922d48bdf9b2a03b09b62ba106a7fb952d368276579cd6ade
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f4100bdf2b90ebe3c9a930e5981dcc99da12efd458602021d41631b9f61056ad6986ca1e93bc26f639e43977beebc2b73b7fe95f6d7a993b9f221a141fc015d8
|
|
7
|
+
data.tar.gz: f80229f22ac0201a35ba6582a1de2a50692ac118da9dc100df638df85f13517fdf2a00396bce4ada9ae32f8c502c8a6a38d1e5f1f4d3cb7b9cff1ae0feb339f1
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.2.0] - 2026-09-29
|
|
4
|
+
|
|
5
|
+
- Fix `require "jsg"`: the library moved from the gem root to `lib/jsg.rb`, so it is on the load path.
|
|
6
|
+
- Fix the syntax additions never running: `JSGPatch` is now prepended to `JS::Object` instead of included. The js gem defines `method_missing`, `respond_to_missing?`, `to_a` and `nil?` on `JS::Object` itself, which shadowed the included module. With js 2.10 (where `JS::Object` is a `BasicObject`) 0.1.x also failed to load with `uninitialized constant JS::Object::JSGPatch`.
|
|
7
|
+
- Rewrite the helpers so they no longer call themselves recursively once active (`to_a`, `isJSArray`, `__props`).
|
|
8
|
+
- `?` methods use JavaScript truthiness. `undefined?` is only true for `undefined`. `each` also iterates array-like objects such as a `NodeList`.
|
|
9
|
+
- A capitalized name without arguments returns the constructor (`JS.global.URLSearchParams.new(...)`). Other JavaScript errors are no longer swallowed.
|
|
10
|
+
- Add `tap` to `JS::Object` again (lost when js 2.10 made it a `BasicObject`), so `createElement("h2").tap { ... }` works.
|
|
11
|
+
- Remove the `==` override. The js gem already compares with `true`, `false` and `nil`.
|
|
12
|
+
- The template's index.html loads `src/main.rb` with `JS::RequireRemote` instead of overriding `require_relative` globally. `rbwasm build` never packed `src/` into the wasm file.
|
|
13
|
+
- Update the template to ruby.wasm 2.10.1 (browser script, `js` and `ruby_wasm` gems). `jsg build` builds Ruby 4.0 with `bundle exec rbwasm`.
|
|
14
|
+
- `jsg new` reports errors instead of silently ignoring them, and gets a `--skip-build` option. `jsg server` accepts a port and explains how to install WEBrick when it is missing. New `jsg version`.
|
|
15
|
+
- The gem depends on `js` and `ruby_wasm` `~> 2.10` on every platform, and no longer on `erb` (a default gem).
|
|
16
|
+
- Add a README and tests (rspec for the command line tool, ruby.wasm on Node.js for `lib/jsg.rb`).
|
|
17
|
+
|
|
18
|
+
## [0.1.2] - 2024-05-18
|
|
19
|
+
|
|
20
|
+
- Don't require
|
|
21
|
+
|
|
3
22
|
## [0.1.0] - 2024-05-09
|
|
4
23
|
|
|
5
24
|
- Initial release
|
data/README.md
CHANGED
|
@@ -1,34 +1,110 @@
|
|
|
1
|
-
#
|
|
1
|
+
# jsg
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
jsg helps you set up [ruby.wasm](https://github.com/ruby/ruby.wasm) browser projects, and it adds a friendlier syntax on top of the [js gem](https://rubygems.org/gems/js).
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
It has two parts:
|
|
6
|
+
|
|
7
|
+
- the `jsg` command, which creates, builds and serves a project
|
|
8
|
+
- `require "jsg"` inside the browser, which lets you write `document.title` instead of `JS.global[:document][:title]`
|
|
9
|
+
|
|
10
|
+
jsg 0.2 targets ruby.wasm 2.10.1 and builds Ruby 4.0 for the browser.
|
|
6
11
|
|
|
7
12
|
## Installation
|
|
8
13
|
|
|
9
|
-
|
|
14
|
+
```bash
|
|
15
|
+
gem install jsg
|
|
16
|
+
```
|
|
10
17
|
|
|
11
|
-
|
|
18
|
+
This also installs `ruby_wasm`, which provides the `rbwasm` build tool. `jsg server` needs WEBrick, which is not a default gem anymore:
|
|
12
19
|
|
|
13
|
-
|
|
20
|
+
```bash
|
|
21
|
+
gem install webrick
|
|
22
|
+
```
|
|
14
23
|
|
|
15
|
-
|
|
24
|
+
## Creating a project
|
|
16
25
|
|
|
17
|
-
|
|
26
|
+
```bash
|
|
27
|
+
jsg new myapp # copy the template into ./myapp, then run jsg build
|
|
28
|
+
cd myapp
|
|
29
|
+
jsg server # serve the folder on http://localhost:8080 (or the next free port)
|
|
30
|
+
```
|
|
18
31
|
|
|
19
|
-
|
|
32
|
+
`jsg new myapp --skip-build` only copies the template. The project contains:
|
|
20
33
|
|
|
21
|
-
|
|
34
|
+
- `index.html`, with a `<script type="text/ruby">` block that requires jsg and loads `src/main.rb`
|
|
35
|
+
- `src/main.rb`, the place for your code
|
|
36
|
+
- `assets/browser.script.iife.js`, ruby.wasm's browser script, set up to load `assets/ruby-app.wasm`
|
|
37
|
+
- `Gemfile` with `ruby_wasm`, `js` and `jsg`
|
|
38
|
+
- a `.gitignore` for the `build/` and `rubies/` folders
|
|
22
39
|
|
|
23
|
-
|
|
40
|
+
`jsg build` runs `bundle install` and then `bundle exec rbwasm build --ruby-version 4.0 -o assets/ruby-app.wasm`. The wasm file contains Ruby and the gems from the Gemfile, not your own files: `index.html` fetches `src/main.rb` from the web server with `JS::RequireRemote`, so a page reload picks up your changes. Run `jsg build` again only when you change the Gemfile. The first build compiles CRuby and the gems for WebAssembly and takes a while. Later builds reuse the `build/` and `rubies/` folders.
|
|
41
|
+
|
|
42
|
+
## Syntax
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
require "jsg"
|
|
46
|
+
|
|
47
|
+
d = JSG.document
|
|
48
|
+
d.getElementById("spinner").style.display = "none"
|
|
49
|
+
|
|
50
|
+
d.createElement("h2").tap do |h2|
|
|
51
|
+
h2.innerText = "Examples"
|
|
52
|
+
d.body.appendChild(h2)
|
|
53
|
+
end
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
What `require "jsg"` adds to `JS::Object`:
|
|
24
57
|
|
|
25
|
-
|
|
58
|
+
| You write | Result |
|
|
59
|
+
| --- | --- |
|
|
60
|
+
| `element.innerText` | property value, converted with `to_rb` |
|
|
61
|
+
| `element.innerText = "Hi"` | sets the property (the value is converted with `to_js`) |
|
|
62
|
+
| `document.getElementById("x")` | calls the method, result converted with `to_rb` |
|
|
63
|
+
| `element.hidden?`, `list.includes?(3)` | `true`/`false`, using JavaScript truthiness |
|
|
64
|
+
| `JS.global.URLSearchParams.new("a=1")` | a capitalized name without arguments returns the constructor |
|
|
65
|
+
| `obj.missing` | a property that is `undefined` raises `NoMethodError` |
|
|
26
66
|
|
|
27
|
-
|
|
67
|
+
`to_rb` converts JavaScript values to Ruby:
|
|
68
|
+
|
|
69
|
+
| JavaScript | Ruby |
|
|
70
|
+
| --- | --- |
|
|
71
|
+
| number | `Float` (`3` becomes `3.0`) |
|
|
72
|
+
| string | `String` |
|
|
73
|
+
| boolean | `true` / `false` |
|
|
74
|
+
| bigint | `Integer` |
|
|
75
|
+
| symbol | `Symbol` |
|
|
76
|
+
| `null` | `nil` |
|
|
77
|
+
| Array | `Array`, elements converted too |
|
|
78
|
+
| anything else, including `undefined` | stays a `JS::Object` |
|
|
79
|
+
|
|
80
|
+
More helpers:
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
JSG.window # JS.global, also JSG.w
|
|
84
|
+
JSG.document # JS.global[:document], also JSG.d
|
|
85
|
+
JSG.q("p") # document.querySelectorAll("p")
|
|
86
|
+
JSG.global # anything else is forwarded to the JS module
|
|
87
|
+
|
|
88
|
+
list = JSG.q("li")
|
|
89
|
+
list.to_a # array-like objects (NodeList, ...) to a Ruby Array
|
|
90
|
+
list.each { |li| li.classList.add("done") }
|
|
91
|
+
obj.to_a(convertTypes: false) # keep the elements as JS::Object
|
|
92
|
+
|
|
93
|
+
value.nil? # true for null and undefined
|
|
94
|
+
value.undefined? # true only for undefined
|
|
95
|
+
value.typeof?(:string)
|
|
96
|
+
el.tap { |e| ... } # like Kernel#tap (JS::Object is a BasicObject since js 2.10)
|
|
97
|
+
value == true # comparing with true, false and nil works (JavaScript's == rules)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Development
|
|
28
101
|
|
|
29
|
-
|
|
102
|
+
```bash
|
|
103
|
+
bin/setup
|
|
104
|
+
bundle exec rake # rspec, the ruby.wasm tests and rubocop
|
|
105
|
+
```
|
|
30
106
|
|
|
31
|
-
|
|
107
|
+
The specs in `spec/` test the command line tool on your normal Ruby. `lib/jsg.rb` needs the js gem, which only works inside ruby.wasm, so its tests in `test/wasm` run the prebuilt ruby.wasm on Node.js (`bundle exec rake test:wasm`, needs Node.js and npm).
|
|
32
108
|
|
|
33
109
|
## License
|
|
34
110
|
|
data/exe/jsg
CHANGED
|
@@ -1,98 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
require_relative "../lib/jsg/cli"
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
require "fileutils"
|
|
7
|
-
|
|
8
|
-
module JSG
|
|
9
|
-
# create a new project at project_name
|
|
10
|
-
def self.create_project(project_name)
|
|
11
|
-
base_path = File.expand_path("../template", __dir__)
|
|
12
|
-
destination = File.join(Dir.pwd, project_name)
|
|
13
|
-
|
|
14
|
-
if Dir.exist?(destination)
|
|
15
|
-
raise "Sorry #{destination} already exists, please choose other name or delete the folder"
|
|
16
|
-
end
|
|
17
|
-
FileUtils.mkdir_p(destination)
|
|
18
|
-
|
|
19
|
-
Dir.glob("#{base_path}/**/*").each do |file_path|
|
|
20
|
-
next if File.directory?(file_path)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
content = File.read(file_path)
|
|
24
|
-
result = if file_path.end_with?(".erb")
|
|
25
|
-
template = ERB.new(content)
|
|
26
|
-
template.result(binding)
|
|
27
|
-
else
|
|
28
|
-
content
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
relative_path = file_path.sub("#{base_path}/", "").gsub(/\.erb$/, "")
|
|
32
|
-
new_file_path = File.join(destination, relative_path)
|
|
33
|
-
|
|
34
|
-
p new_file_path
|
|
35
|
-
|
|
36
|
-
new_file_path.gsub!("gitignore", ".gitignore")
|
|
37
|
-
FileUtils.mkdir_p(File.dirname(new_file_path))
|
|
38
|
-
File.write(new_file_path, result)
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def self.build
|
|
43
|
-
system("bundle install")
|
|
44
|
-
system("rbwasm build -o assets/ruby-app.wasm")
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def self.server
|
|
48
|
-
require 'webrick'
|
|
49
|
-
|
|
50
|
-
port = 8080
|
|
51
|
-
|
|
52
|
-
loop do
|
|
53
|
-
begin
|
|
54
|
-
server = WEBrick::HTTPServer.new(BindAddres: '0.0.0.0', :Port => port, :DocumentRoot => Dir.pwd)
|
|
55
|
-
trap('INT') { server.shutdown }
|
|
56
|
-
puts "Serving at port http://0.0.0.0.:#{port}"
|
|
57
|
-
server.start
|
|
58
|
-
break
|
|
59
|
-
rescue Errno::EADDRINUSE
|
|
60
|
-
port += 1
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
command = ARGV.shift # First argument is the command
|
|
68
|
-
|
|
69
|
-
case command
|
|
70
|
-
when "new"
|
|
71
|
-
#options = parse_options
|
|
72
|
-
project_name = ARGV.shift # Next argument should be the project name
|
|
73
|
-
|
|
74
|
-
if project_name
|
|
75
|
-
begin
|
|
76
|
-
JSG.create_project(project_name)
|
|
77
|
-
# check if we have an internet connection or think about caching this somewhere.
|
|
78
|
-
Dir.chdir(project_name) do
|
|
79
|
-
JSG.build
|
|
80
|
-
end
|
|
81
|
-
rescue StandardError => e
|
|
82
|
-
end
|
|
83
|
-
else
|
|
84
|
-
puts "Error: Project name is required."
|
|
85
|
-
#puts "For help, run: jsg new --help"
|
|
86
|
-
exit 1
|
|
87
|
-
end
|
|
88
|
-
when "build"
|
|
89
|
-
JSG.build
|
|
90
|
-
when "serve", "server"
|
|
91
|
-
JSG.server
|
|
92
|
-
else
|
|
93
|
-
puts "Unknown command: #{command}"
|
|
94
|
-
puts "Use 'jsg new projectname' for creating new projects."
|
|
95
|
-
puts "Use 'jsg build to build the .wasm file of the project."
|
|
96
|
-
puts "Use 'jsg server to start a server."
|
|
97
|
-
exit 1
|
|
98
|
-
end
|
|
6
|
+
exit JSG::CLI.run(ARGV.dup)
|
data/lib/jsg/cli.rb
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "erb"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require_relative "version"
|
|
6
|
+
|
|
7
|
+
module JSG
|
|
8
|
+
class Error < StandardError; end
|
|
9
|
+
|
|
10
|
+
# The `jsg` command line tool. Runs on the host, not inside ruby.wasm,
|
|
11
|
+
# so it must not require "js".
|
|
12
|
+
module CLI
|
|
13
|
+
TEMPLATE_PATH = File.expand_path("../../template", __dir__)
|
|
14
|
+
# Ruby version that `jsg build` compiles to WebAssembly. Matches the
|
|
15
|
+
# @ruby/4.0-wasm-wasi browser script in template/assets.
|
|
16
|
+
WASM_RUBY_VERSION = "4.0"
|
|
17
|
+
|
|
18
|
+
USAGE = <<~TEXT
|
|
19
|
+
Usage:
|
|
20
|
+
jsg new PROJECT [--skip-build] create a new project (and build it)
|
|
21
|
+
jsg build bundle install, then build assets/ruby-app.wasm (Ruby 4.0)
|
|
22
|
+
jsg server [PORT] serve the current directory (default port 8080)
|
|
23
|
+
jsg version print the jsg version
|
|
24
|
+
TEXT
|
|
25
|
+
|
|
26
|
+
module_function
|
|
27
|
+
|
|
28
|
+
def run(argv)
|
|
29
|
+
command = argv.shift
|
|
30
|
+
case command
|
|
31
|
+
when "new"
|
|
32
|
+
skip_build = !argv.delete("--skip-build").nil?
|
|
33
|
+
project_name = argv.shift
|
|
34
|
+
return usage_error("Error: Project name is required.") unless project_name
|
|
35
|
+
|
|
36
|
+
destination = create_project(project_name)
|
|
37
|
+
Dir.chdir(destination) { build } unless skip_build
|
|
38
|
+
when "build"
|
|
39
|
+
build
|
|
40
|
+
when "serve", "server"
|
|
41
|
+
server(port: Integer(argv.shift || 8080))
|
|
42
|
+
when "version", "-v", "--version"
|
|
43
|
+
puts VERSION
|
|
44
|
+
else
|
|
45
|
+
return usage_error("Unknown command: #{command}") if command
|
|
46
|
+
|
|
47
|
+
puts USAGE
|
|
48
|
+
end
|
|
49
|
+
0
|
|
50
|
+
rescue Error => e
|
|
51
|
+
warn e.message
|
|
52
|
+
1
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def usage_error(message)
|
|
56
|
+
warn message
|
|
57
|
+
warn USAGE
|
|
58
|
+
1
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Copies the template into dir/project_name. Files ending in .erb are
|
|
62
|
+
# rendered with project_name available. Returns the destination path.
|
|
63
|
+
def create_project(project_name, dir: Dir.pwd)
|
|
64
|
+
destination = File.join(dir, project_name)
|
|
65
|
+
if File.exist?(destination)
|
|
66
|
+
raise Error, "Sorry, #{destination} already exists. Please choose another name or delete the folder."
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
Dir.glob("**/*", base: TEMPLATE_PATH).sort.each do |relative_path|
|
|
70
|
+
source = File.join(TEMPLATE_PATH, relative_path)
|
|
71
|
+
next if File.directory?(source)
|
|
72
|
+
|
|
73
|
+
content = File.read(source)
|
|
74
|
+
content = ERB.new(content).result_with_hash(project_name: project_name) if relative_path.end_with?(".erb")
|
|
75
|
+
|
|
76
|
+
target = relative_path.delete_suffix(".erb")
|
|
77
|
+
target = target.sub(%r{(\A|/)gitignore\z}, "\\1.gitignore")
|
|
78
|
+
target_path = File.join(destination, target)
|
|
79
|
+
FileUtils.mkdir_p(File.dirname(target_path))
|
|
80
|
+
File.write(target_path, content)
|
|
81
|
+
puts "create #{File.join(project_name, target)}"
|
|
82
|
+
end
|
|
83
|
+
destination
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def build
|
|
87
|
+
system("bundle", "install") or raise Error, "bundle install failed"
|
|
88
|
+
system("bundle", "exec", "rbwasm", "build", "--ruby-version", WASM_RUBY_VERSION, "-o", "assets/ruby-app.wasm") or
|
|
89
|
+
raise Error, "rbwasm build failed"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def server(port: 8080)
|
|
93
|
+
begin
|
|
94
|
+
require "webrick"
|
|
95
|
+
rescue LoadError
|
|
96
|
+
raise Error, "jsg server needs the webrick gem. Install it with: gem install webrick"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
loop do
|
|
100
|
+
server = WEBrick::HTTPServer.new(BindAddress: "0.0.0.0", Port: port, DocumentRoot: Dir.pwd)
|
|
101
|
+
trap("INT") { server.shutdown }
|
|
102
|
+
puts "Serving #{Dir.pwd} at http://localhost:#{port}"
|
|
103
|
+
server.start
|
|
104
|
+
break
|
|
105
|
+
rescue Errno::EADDRINUSE
|
|
106
|
+
port += 1
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
data/lib/jsg/version.rb
CHANGED
data/lib/jsg.rb
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "jsg/version"
|
|
4
|
+
require "js"
|
|
5
|
+
|
|
6
|
+
# Syntax additions for JS::Object.
|
|
7
|
+
#
|
|
8
|
+
# The module is prepended to JS::Object (not included), because the js gem
|
|
9
|
+
# defines method_missing, respond_to_missing?, to_a and (since js 2.10) nil?
|
|
10
|
+
# directly on JS::Object. An included module sits behind the class in the
|
|
11
|
+
# ancestor chain, so those methods would never be reached.
|
|
12
|
+
#
|
|
13
|
+
# The methods in here only use JS::Object#[], #call, #typeof and
|
|
14
|
+
# #strictly_eql? to talk to JavaScript, never the method_missing shortcuts,
|
|
15
|
+
# so they cannot recurse into themselves.
|
|
16
|
+
module JSGPatch
|
|
17
|
+
SETTER = /\A[A-Za-z_]\w*=\z/
|
|
18
|
+
PREDICATE = /\A[A-Za-z_]\w*\?\z/
|
|
19
|
+
|
|
20
|
+
# Methods added to the JS module.
|
|
21
|
+
module ClassMethods
|
|
22
|
+
# JS.try_convert_to_rb(obj) -> Ruby Object or JS::Object
|
|
23
|
+
#
|
|
24
|
+
# Converts the given object to a Ruby object using to_rb. Returns the
|
|
25
|
+
# argument unchanged if it cannot be converted.
|
|
26
|
+
def try_convert_to_rb(obj)
|
|
27
|
+
obj.respond_to?(:to_rb) ? obj.to_rb : obj
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Converts an array-like JavaScript object (Array, NodeList, arguments, ...)
|
|
32
|
+
# to a Ruby Array. The elements are converted with to_rb unless
|
|
33
|
+
# convertTypes is false.
|
|
34
|
+
# rubocop:disable-next Naming/MethodParameterName, Naming/VariableName
|
|
35
|
+
def to_a(convertTypes: true)
|
|
36
|
+
as_array = JS.global[:Array].call(:from, self)
|
|
37
|
+
Array.new(as_array[:length].to_i) do |index|
|
|
38
|
+
item = as_array[index]
|
|
39
|
+
convertTypes ? item.to_rb : item
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Converts JavaScript primitives and arrays to Ruby objects:
|
|
44
|
+
# number -> Float, string -> String, boolean -> true/false,
|
|
45
|
+
# bigint -> Integer, symbol -> Symbol, null -> nil, Array -> Array.
|
|
46
|
+
# Everything else (objects, functions, undefined) is returned as JS::Object.
|
|
47
|
+
def to_rb
|
|
48
|
+
case typeof
|
|
49
|
+
when "number" then to_f
|
|
50
|
+
when "string" then to_s
|
|
51
|
+
when "boolean" then strictly_eql?(JS::True)
|
|
52
|
+
when "bigint" then to_i
|
|
53
|
+
when "symbol" then self[:description].to_s.to_sym
|
|
54
|
+
when "object"
|
|
55
|
+
return nil if strictly_eql?(JS::Null)
|
|
56
|
+
|
|
57
|
+
isJSArray ? to_a : self
|
|
58
|
+
else
|
|
59
|
+
self
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def isJSArray # rubocop:disable Naming/MethodName, Naming/PredicateMethod
|
|
64
|
+
JS.global[:Array].call(:isArray, self).strictly_eql?(JS::True)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def typeof?(type)
|
|
68
|
+
typeof == type.to_s
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Iterates over the elements of an array-like object (anything with a
|
|
72
|
+
# numeric length), otherwise over the property names of the object and its
|
|
73
|
+
# prototype chain.
|
|
74
|
+
def each(&block)
|
|
75
|
+
return Enumerator.new { |yielder| each { |item| yielder << item } } unless block
|
|
76
|
+
|
|
77
|
+
if array_like?
|
|
78
|
+
to_a.each(&block)
|
|
79
|
+
else
|
|
80
|
+
__props.each(&block)
|
|
81
|
+
end
|
|
82
|
+
self
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Kernel#tap, which JS::Object lost when the js gem 2.10 made it a
|
|
86
|
+
# BasicObject. (#then is left out on purpose: Promises have their own.)
|
|
87
|
+
def tap
|
|
88
|
+
yield self
|
|
89
|
+
self
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# true for JavaScript null and undefined
|
|
93
|
+
def nil?
|
|
94
|
+
strictly_eql?(JS::Null) || strictly_eql?(JS::Undefined)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# true only for JavaScript undefined
|
|
98
|
+
def undefined?
|
|
99
|
+
strictly_eql?(JS::Undefined)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Property names of the object and its prototype chain, as Symbols.
|
|
103
|
+
def __props
|
|
104
|
+
object = JS.global[:Object]
|
|
105
|
+
props = []
|
|
106
|
+
current = self
|
|
107
|
+
until current.nil?
|
|
108
|
+
names = object.call(:getOwnPropertyNames, current)
|
|
109
|
+
props.concat(names.to_a)
|
|
110
|
+
current = object.call(:getPrototypeOf, current)
|
|
111
|
+
end
|
|
112
|
+
props.uniq.map(&:to_sym)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# element.innerText = "Hello" # sets a property (value converted with to_js)
|
|
116
|
+
# element.hidden? # property or method result as true/false (JS truthiness)
|
|
117
|
+
# document.title # reads a property, converted with to_rb
|
|
118
|
+
# document.getElementById("x") # calls a method, result converted with to_rb
|
|
119
|
+
# JS.global.URLSearchParams # capitalized name without arguments: returns the constructor
|
|
120
|
+
#
|
|
121
|
+
# Anything else, e.g. a property that is undefined, goes to the js gem's
|
|
122
|
+
# method_missing.
|
|
123
|
+
def method_missing(sym, *args, &block)
|
|
124
|
+
name = sym.to_s
|
|
125
|
+
|
|
126
|
+
if SETTER.match?(name) && args.length == 1 && block.nil?
|
|
127
|
+
self[name[0..-2]] = args[0]
|
|
128
|
+
return args[0]
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
if PREDICATE.match?(name)
|
|
132
|
+
value = self[name[0..-2]]
|
|
133
|
+
value = call(name[0..-2], *args, &block) if value.typeof == "function"
|
|
134
|
+
return JS.global.call(:Boolean, value).strictly_eql?(JS::True)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
value = self[sym]
|
|
138
|
+
case value.typeof
|
|
139
|
+
when "undefined"
|
|
140
|
+
super
|
|
141
|
+
when "function"
|
|
142
|
+
# Calling a class like URLSearchParams without `new` throws, so return
|
|
143
|
+
# the constructor itself and let the caller use `.new(...)`.
|
|
144
|
+
return value if args.empty? && block.nil? && name.match?(/\A[A-Z]/)
|
|
145
|
+
|
|
146
|
+
call(sym, *args, &block).to_rb
|
|
147
|
+
else
|
|
148
|
+
value.to_rb
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def respond_to_missing?(sym, _include_private = false)
|
|
153
|
+
name = sym.to_s
|
|
154
|
+
return true if SETTER.match?(name) || PREDICATE.match?(name)
|
|
155
|
+
return false unless typeof?(:object) || typeof?(:function)
|
|
156
|
+
|
|
157
|
+
!self[sym].undefined?
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
private
|
|
161
|
+
|
|
162
|
+
def array_like?
|
|
163
|
+
return false unless typeof?(:object) && !nil?
|
|
164
|
+
|
|
165
|
+
isJSArray || self[:length].typeof?(:number)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Applying the patch to JS::Object. See the comment on JSGPatch for why this is
|
|
170
|
+
# a prepend and not an include.
|
|
171
|
+
class JS::Object # rubocop:disable Style/ClassAndModuleChildren
|
|
172
|
+
prepend ::JSGPatch
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
module JS
|
|
176
|
+
extend JSGPatch::ClassMethods
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Shortcuts for the global object and the document.
|
|
180
|
+
module JSG
|
|
181
|
+
class Error < StandardError; end
|
|
182
|
+
|
|
183
|
+
def self.window
|
|
184
|
+
JS.global
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def self.document
|
|
188
|
+
JS.global[:document]
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def self.querySelectorAll(*args) # rubocop:disable Naming/MethodName
|
|
192
|
+
document.querySelectorAll(*args)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
singleton_class.alias_method :w, :window
|
|
196
|
+
singleton_class.alias_method :d, :document
|
|
197
|
+
singleton_class.alias_method :q, :querySelectorAll
|
|
198
|
+
|
|
199
|
+
# Forwards everything else to the JS module, e.g. JSG.global or JSG.eval.
|
|
200
|
+
def self.method_missing(method, ...)
|
|
201
|
+
if JS.respond_to?(method)
|
|
202
|
+
JS.send(method, ...)
|
|
203
|
+
else
|
|
204
|
+
super
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def self.respond_to_missing?(method, include_private = false)
|
|
209
|
+
JS.respond_to?(method, include_private) || super
|
|
210
|
+
end
|
|
211
|
+
end
|
data/sig/jsg.rbs
CHANGED
data/template/Gemfile
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
source "https://rubygems.org"
|
|
4
4
|
|
|
5
|
-
#
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
gem "
|
|
9
|
-
gem
|
|
5
|
+
# ruby_wasm provides the rbwasm build tool, js is the JavaScript bridge used
|
|
6
|
+
# inside the browser. Keep both on the same version as
|
|
7
|
+
# assets/browser.script.iife.js (ruby.wasm 2.10.1).
|
|
8
|
+
gem "ruby_wasm", "~> 2.10.1"
|
|
9
|
+
gem "js", "~> 2.10.1"
|
|
10
|
+
gem "jsg", "~> 0.2"
|