dommy-js-quickjs 0.1.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 +7 -0
- data/CHANGELOG.md +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +150 -0
- data/Rakefile +49 -0
- data/docs/bridge-redesign.md +559 -0
- data/docs/wpt-conformance.md +752 -0
- data/lib/dommy/js/constructor_registry.rb +40 -0
- data/lib/dommy/js/custom_elements.rb +55 -0
- data/lib/dommy/js/dom_interfaces.rb +139 -0
- data/lib/dommy/js/handle_table.rb +52 -0
- data/lib/dommy/js/host_bridge.rb +400 -0
- data/lib/dommy/js/host_runtime.js +922 -0
- data/lib/dommy/js/observable_runtime.js +728 -0
- data/lib/dommy/js/quickjs/backend.rb +64 -0
- data/lib/dommy/js/quickjs/capybara.rb +80 -0
- data/lib/dommy/js/quickjs/runtime.rb +210 -0
- data/lib/dommy/js/quickjs/version.rb +9 -0
- data/lib/dommy/js/quickjs/wasm_bridge.rb +151 -0
- data/lib/dommy/js/quickjs.rb +20 -0
- data/sig/dommy/js/quickjs.rbs +8 -0
- metadata +95 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "quickjs"
|
|
4
|
+
|
|
5
|
+
module Dommy
|
|
6
|
+
module Js
|
|
7
|
+
module Quickjs
|
|
8
|
+
# Binds HostBridge's abstract backend contract to the `quickjs` gem.
|
|
9
|
+
class Backend
|
|
10
|
+
# The gem's default eval timeout is 100ms, which interrupts large
|
|
11
|
+
# synchronous bridge loops (every property crossing is a Ruby call).
|
|
12
|
+
DEFAULT_TIMEOUT_MSEC = 60_000
|
|
13
|
+
|
|
14
|
+
def initialize(**vm_opts)
|
|
15
|
+
vm_opts = {timeout_msec: DEFAULT_TIMEOUT_MSEC}.merge(vm_opts)
|
|
16
|
+
@vm = ::Quickjs::VM.new(**vm_opts)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def eval(js)
|
|
20
|
+
@vm.eval_code(js, async: false)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Async eval: the gem awaits the top-level result and drains the
|
|
24
|
+
# microtask queue, so JS `await`/Promises resolve before returning.
|
|
25
|
+
def eval_awaited(js)
|
|
26
|
+
@vm.eval_code(js, async: true)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def define_host_function(name, &block)
|
|
30
|
+
@vm.define_function(name, &block)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def call_js(path, *args)
|
|
34
|
+
@vm.call(path, *args)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def drain_microtasks
|
|
38
|
+
@vm.drain_jobs!
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Register a handler for promise rejections that reach the microtask
|
|
42
|
+
# queue with no `.catch` — frameworks (Turbo, …) often swallow these,
|
|
43
|
+
# so surfacing them is essential for diagnosing failures.
|
|
44
|
+
def on_unhandled_rejection(&block)
|
|
45
|
+
@vm.on_unhandled_rejection(&block)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Register a handler for console.(log|info|debug|warn|error). The block
|
|
49
|
+
# receives a log object (#severity / #to_s / #raw).
|
|
50
|
+
def on_log(&block)
|
|
51
|
+
@vm.on_log(&block)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def run_gc
|
|
55
|
+
@vm.gc!
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def dispose
|
|
59
|
+
@vm.dispose!
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "capybara/dommy"
|
|
4
|
+
require_relative "../quickjs"
|
|
5
|
+
|
|
6
|
+
module Dommy
|
|
7
|
+
module Js
|
|
8
|
+
module Quickjs
|
|
9
|
+
# Opt-in Capybara integration. Requiring this file enables JS execution on
|
|
10
|
+
# Capybara::Dommy::Driver (via install_capybara! below), so execute_script /
|
|
11
|
+
# evaluate_script run against the current Dommy document through a QuickJS
|
|
12
|
+
# Runtime. Without this require, capybara-dommy stays JS-free (its default).
|
|
13
|
+
module CapybaraDriver
|
|
14
|
+
def execute_script(script, *_args)
|
|
15
|
+
dommy_js_runtime.execute(script)
|
|
16
|
+
nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def evaluate_script(script, *_args)
|
|
20
|
+
decode_for_capybara(dommy_js_runtime.evaluate(script))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# No real async loop; evaluate synchronously. Sufficient for scripts
|
|
24
|
+
# that resolve immediately (the common Capybara case).
|
|
25
|
+
def evaluate_async_script(script, *args)
|
|
26
|
+
evaluate_script(script, *args)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
# One Runtime per document. Rebuilt when navigation swaps the document
|
|
32
|
+
# so JS always sees the current page (and the old VM is released).
|
|
33
|
+
def dommy_js_runtime
|
|
34
|
+
doc = document
|
|
35
|
+
unless defined?(@dommy_js_doc) && @dommy_js_doc.equal?(doc)
|
|
36
|
+
@dommy_js_runtime&.dispose
|
|
37
|
+
@dommy_js_runtime = Runtime.new
|
|
38
|
+
@dommy_js_runtime.define_host_object("document", doc)
|
|
39
|
+
view = doc.default_view
|
|
40
|
+
@dommy_js_runtime.install_window(view) if view
|
|
41
|
+
@dommy_js_doc = doc
|
|
42
|
+
end
|
|
43
|
+
@dommy_js_runtime
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Map an evaluate() result to what Capybara expects:
|
|
47
|
+
# - Array -> recurse (so element collections wrap per item)
|
|
48
|
+
# - JS undefined -> nil
|
|
49
|
+
# - Dommy::Element -> Capybara::Dommy::Node (covers HTML/SVG subclasses)
|
|
50
|
+
# - other bridge obj -> nil (Document/Text/Comment/Fragment/NodeList/
|
|
51
|
+
# Window have no Capybara representation; a browser
|
|
52
|
+
# likewise returns non-serializable values as null)
|
|
53
|
+
# - primitive/Hash -> as-is
|
|
54
|
+
def decode_for_capybara(value)
|
|
55
|
+
case value
|
|
56
|
+
when Array
|
|
57
|
+
value.map { |element| decode_for_capybara(element) }
|
|
58
|
+
when ::Quickjs::Value::UNDEFINED
|
|
59
|
+
nil
|
|
60
|
+
when ::Dommy::Element
|
|
61
|
+
::Capybara::Dommy::Node.new(self, value)
|
|
62
|
+
else
|
|
63
|
+
value.respond_to?(:__js_get__) ? nil : value
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Idempotently prepend JS-execution support onto Capybara::Dommy::Driver.
|
|
69
|
+
# Safe to call multiple times; only prepends once. Called on require, but
|
|
70
|
+
# exposed so integration can be enabled/controlled explicitly (e.g. tests).
|
|
71
|
+
def self.install_capybara!
|
|
72
|
+
return if ::Capybara::Dommy::Driver.ancestors.include?(CapybaraDriver)
|
|
73
|
+
|
|
74
|
+
::Capybara::Dommy::Driver.prepend(CapybaraDriver)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
Dommy::Js::Quickjs.install_capybara!
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dommy
|
|
4
|
+
module Js
|
|
5
|
+
module Quickjs
|
|
6
|
+
# Public entry point: a JS runtime that can drive a Dommy DOM.
|
|
7
|
+
#
|
|
8
|
+
# rt = Dommy::Js::Quickjs::Runtime.new
|
|
9
|
+
# rt.define_host_object("document", win.document)
|
|
10
|
+
# rt.evaluate('document.querySelector("h1").textContent') #=> "..."
|
|
11
|
+
#
|
|
12
|
+
# Wires the QuickJS Backend to the engine-agnostic HostBridge, seeded with
|
|
13
|
+
# the Dommy method manifest.
|
|
14
|
+
class Runtime
|
|
15
|
+
def initialize(**vm_opts)
|
|
16
|
+
@backend = Backend.new(**vm_opts)
|
|
17
|
+
@bridge = Dommy::Js::HostBridge.new(@backend)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def define_host_object(name, obj)
|
|
21
|
+
@bridge.define_host_object(name, obj)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Inject the Dommy window and alias the bare browser timer globals to it,
|
|
25
|
+
# so `setTimeout(fn, ms)` routes into Dommy's deterministic scheduler.
|
|
26
|
+
# Drive callbacks with `win.scheduler.advance_time(ms)`. `window.setTimeout`
|
|
27
|
+
# already works via the Window manifest; this also wires the unqualified
|
|
28
|
+
# globals browsers expose.
|
|
29
|
+
def install_window(win)
|
|
30
|
+
@window = win
|
|
31
|
+
define_host_object("window", win)
|
|
32
|
+
@bridge.window = win
|
|
33
|
+
@backend.eval(<<~JS)
|
|
34
|
+
globalThis.setTimeout = (fn, delay) => window.setTimeout(fn, delay);
|
|
35
|
+
globalThis.clearTimeout = (id) => window.clearTimeout(id);
|
|
36
|
+
globalThis.setInterval = (fn, delay) => window.setInterval(fn, delay);
|
|
37
|
+
globalThis.clearInterval = (id) => window.clearInterval(id);
|
|
38
|
+
globalThis.requestAnimationFrame = (fn) => window.requestAnimationFrame(fn);
|
|
39
|
+
globalThis.cancelAnimationFrame = (id) => window.cancelAnimationFrame(id);
|
|
40
|
+
// queueMicrotask must share the engine's promise-job (microtask)
|
|
41
|
+
// queue so its callbacks are FIFO-ordered with Promise reactions
|
|
42
|
+
// (the WHATWG single-microtask-queue model). Routing through the
|
|
43
|
+
// Ruby scheduler instead would drain on a separate pass, reordering
|
|
44
|
+
// it after all native promise jobs.
|
|
45
|
+
globalThis.queueMicrotask = (fn) => {
|
|
46
|
+
if (typeof fn !== "function") throw new TypeError("queueMicrotask requires a function");
|
|
47
|
+
Promise.resolve().then(() => { fn(); });
|
|
48
|
+
};
|
|
49
|
+
JS
|
|
50
|
+
win
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Expose the seeded interface constructors on a secondary window (an
|
|
54
|
+
# iframe's contentWindow), so cross-window instanceof / defaultView work.
|
|
55
|
+
# Call after install_window (the constructors must already be seeded).
|
|
56
|
+
def expose_constructors_on(window_obj)
|
|
57
|
+
@bridge.expose_constructors_on(window_obj)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Run a script for side effects (no return value). Wrapped in an IIFE so
|
|
61
|
+
# statements are allowed and the completion value is voided — otherwise a
|
|
62
|
+
# trailing Promise expression would trip the gem's "unawaited Promise"
|
|
63
|
+
# guard. Drains microtasks so queued .then work lands before returning.
|
|
64
|
+
def execute(js)
|
|
65
|
+
@backend.eval("(function () {\n#{js}\n})();")
|
|
66
|
+
drain_microtasks
|
|
67
|
+
nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Evaluate JS and return its value, with DOM nodes decoded to Dommy
|
|
71
|
+
# objects (rather than the empty Hash a raw proxy becomes crossing to
|
|
72
|
+
# Ruby). Accepts either an expression (`document.title`) or a statement
|
|
73
|
+
# body that uses `return` (`const x = ...; return x;`): the expression
|
|
74
|
+
# form is tried first and, on a syntax error, retried as an async
|
|
75
|
+
# function body. Syntax errors are compile-time so the failed first
|
|
76
|
+
# attempt runs nothing. The result is awaited, so a Promise resolves
|
|
77
|
+
# before returning.
|
|
78
|
+
def evaluate(js)
|
|
79
|
+
@bridge.decode(eval_tagged("await (#{js.strip.sub(/;\s*\z/, "")})"))
|
|
80
|
+
rescue ::Quickjs::SyntaxError
|
|
81
|
+
@bridge.decode(eval_tagged("await (async () => {\n#{js}\n})()"))
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def drain_microtasks
|
|
85
|
+
@backend.drain_microtasks
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Handle-oriented JS access for a wasm guest (see WasmBridge). Memoized
|
|
89
|
+
# so the guest's `__rbWasmInvoke` dispatcher (installed via #on_invoke)
|
|
90
|
+
# stays registered for the VM's lifetime.
|
|
91
|
+
def wasm_bridge
|
|
92
|
+
@wasm_bridge ||= WasmBridge.new(@backend)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Drive the event loop to quiescence: drain the native microtask queue,
|
|
96
|
+
# then advance the deterministic scheduler to its next due timer and drain
|
|
97
|
+
# again, repeating until no timer is pending. This is the single
|
|
98
|
+
# deterministic "settle everything" entry point a host uses after an eval
|
|
99
|
+
# (mirroring a `drain_async!`): every queued microtask runs and every
|
|
100
|
+
# scheduled timer fires, in WHATWG order (microtasks before each timer).
|
|
101
|
+
# `max_iterations` bounds runaway timer loops (e.g. a self-rescheduling
|
|
102
|
+
# setInterval).
|
|
103
|
+
def run_until_idle(max_iterations: 1000)
|
|
104
|
+
sched = @window&.scheduler
|
|
105
|
+
max_iterations.times do
|
|
106
|
+
drain_microtasks
|
|
107
|
+
break unless sched
|
|
108
|
+
|
|
109
|
+
next_at = sched.next_due_timer_at
|
|
110
|
+
break unless next_at
|
|
111
|
+
|
|
112
|
+
sched.advance_time(next_at - sched.now_ms)
|
|
113
|
+
drain_microtasks
|
|
114
|
+
end
|
|
115
|
+
self
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Surface otherwise-swallowed JS promise rejections (see Backend).
|
|
119
|
+
def on_unhandled_rejection(&block)
|
|
120
|
+
@backend.on_unhandled_rejection(&block)
|
|
121
|
+
self
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Observe console.* output (see Backend).
|
|
125
|
+
def on_log(&block)
|
|
126
|
+
@backend.on_log(&block)
|
|
127
|
+
self
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Wire the bare browser globals frameworks reach for, aliased onto the
|
|
131
|
+
# installed window: self / location / history / navigator / storages /
|
|
132
|
+
# CSS / fetch / addEventListener / .... Call after install_window. This
|
|
133
|
+
# is what lets real frontend bundles (Turbo, …) run unmodified.
|
|
134
|
+
def install_browser_globals
|
|
135
|
+
@backend.eval(<<~JS)
|
|
136
|
+
globalThis.self = globalThis;
|
|
137
|
+
// Top-level window: parent/top are the window itself (spec), so
|
|
138
|
+
// frame-walking loops terminate instead of dereferencing undefined.
|
|
139
|
+
globalThis.parent = globalThis;
|
|
140
|
+
globalThis.top = globalThis;
|
|
141
|
+
globalThis.location = window.location;
|
|
142
|
+
globalThis.history = window.history;
|
|
143
|
+
globalThis.navigator = window.navigator;
|
|
144
|
+
globalThis.sessionStorage = window.sessionStorage;
|
|
145
|
+
globalThis.localStorage = window.localStorage;
|
|
146
|
+
globalThis.CSS = window.CSS;
|
|
147
|
+
globalThis.fetch = (...args) => window.fetch(...args);
|
|
148
|
+
globalThis.addEventListener = (...args) => window.addEventListener(...args);
|
|
149
|
+
globalThis.removeEventListener = (...args) => window.removeEventListener(...args);
|
|
150
|
+
globalThis.dispatchEvent = (event) => window.dispatchEvent(event);
|
|
151
|
+
// The window IS the global object, so JS built-in constructors and
|
|
152
|
+
// namespaces are also `window` properties (`window.String`,
|
|
153
|
+
// `window.Number`, …). Mirror them as own props on the window proxy
|
|
154
|
+
// so code that reads constructors off `window` (e.g. the WPT
|
|
155
|
+
// reflection harness's `window[type]` casts) resolves them.
|
|
156
|
+
for (const __n of [
|
|
157
|
+
"String", "Boolean", "Number", "BigInt", "Symbol", "Object", "Array",
|
|
158
|
+
"Function", "Date", "RegExp", "Promise", "Map", "Set", "WeakMap",
|
|
159
|
+
"WeakSet", "Math", "JSON", "Reflect", "Proxy", "Error", "TypeError",
|
|
160
|
+
"RangeError", "SyntaxError", "Infinity", "NaN", "undefined",
|
|
161
|
+
"parseInt", "parseFloat", "isNaN", "isFinite", "globalThis",
|
|
162
|
+
]) {
|
|
163
|
+
try { window[__n] = globalThis[__n]; } catch (__e) {}
|
|
164
|
+
}
|
|
165
|
+
// Minimal WebAssembly.Memory: the engine provides a real
|
|
166
|
+
// SharedArrayBuffer but no WebAssembly, and WPT's `common/sab.js`
|
|
167
|
+
// derives the SAB constructor from
|
|
168
|
+
// `new WebAssembly.Memory({shared:true}).buffer.constructor`. A
|
|
169
|
+
// Memory whose `.buffer` is a SharedArrayBuffer is enough to let
|
|
170
|
+
// those tests (encodeInto, TextDecoder copy, …) exercise shared
|
|
171
|
+
// buffers with the real codec logic.
|
|
172
|
+
if (typeof globalThis.WebAssembly === "undefined" && typeof globalThis.SharedArrayBuffer === "function") {
|
|
173
|
+
globalThis.WebAssembly = {
|
|
174
|
+
Memory: function (opts) {
|
|
175
|
+
const bytes = ((opts && opts.initial) || 0) * 65536;
|
|
176
|
+
this.buffer = (opts && opts.shared)
|
|
177
|
+
? new SharedArrayBuffer(bytes)
|
|
178
|
+
: new ArrayBuffer(bytes);
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
JS
|
|
183
|
+
self
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Run JS GC then drain, so FinalizationRegistry cleanup callbacks fire and
|
|
187
|
+
# release handles for proxies that are no longer referenced.
|
|
188
|
+
def collect_garbage
|
|
189
|
+
@backend.run_gc
|
|
190
|
+
@backend.drain_microtasks
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Live handle count (introspection for lifetime tests).
|
|
194
|
+
def registered_count
|
|
195
|
+
@bridge.registered_count
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def dispose
|
|
199
|
+
@backend.dispose
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
private
|
|
203
|
+
|
|
204
|
+
def eval_tagged(inner_expr)
|
|
205
|
+
@backend.eval_awaited("__rbHost.tag(#{inner_expr});")
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dommy
|
|
4
|
+
module Js
|
|
5
|
+
module Quickjs
|
|
6
|
+
# Handle-oriented JS access for a wasm guest (e.g. mruby-in-wasm under
|
|
7
|
+
# wasmtime-rb). Distinct from the Proxy-based HostBridge: instead of
|
|
8
|
+
# exposing Ruby DOM objects to JS as proxies, this lets a guest treat any
|
|
9
|
+
# JS value as an opaque integer ref it can get/set/call/new on — the shape
|
|
10
|
+
# the guest's `js_*` bridge imports need.
|
|
11
|
+
#
|
|
12
|
+
# The JS half lives in host_runtime.js (the `wasm*` functions on
|
|
13
|
+
# `__rbHost`); this is the thin Ruby facade over them. Every non-primitive
|
|
14
|
+
# JS value crosses as a `JSValue` (an opaque ref into the VM); primitives
|
|
15
|
+
# cross as plain Ruby values. Callbacks the guest registers become JS
|
|
16
|
+
# functions (also refs) that route back through `__rbWasmInvoke`.
|
|
17
|
+
class WasmBridge
|
|
18
|
+
# An opaque handle to a JS value living in the VM. `ref` is the integer
|
|
19
|
+
# id into the JS-side jsRefs table.
|
|
20
|
+
JSValue = Struct.new(:ref) do
|
|
21
|
+
def to_s
|
|
22
|
+
"#<JSValue ref=#{ref}>"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def initialize(backend)
|
|
27
|
+
@backend = backend
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Install the dispatcher JS callbacks route back through. The block
|
|
31
|
+
# receives (invoke_id, packed_args) and must return a packed result
|
|
32
|
+
# (the same tagged shape #pack produces). Called once by the embedder.
|
|
33
|
+
def on_invoke(&block)
|
|
34
|
+
@backend.define_host_function("__rbWasmInvoke") do |invoke_id, args|
|
|
35
|
+
block.call(invoke_id.to_i, args)
|
|
36
|
+
end
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# A ref to the VM's globalThis — the guest's `js_global`.
|
|
41
|
+
def global_ref
|
|
42
|
+
unpack(@backend.call_js("__rbHost.wasmGlobalRef"))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Evaluate real JS source in global scope; returns the (packed) result.
|
|
46
|
+
def eval_js(src)
|
|
47
|
+
unpack(@backend.call_js("__rbHost.wasmEval", src.to_s))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def get(recv, prop)
|
|
51
|
+
unpack(@backend.call_js("__rbHost.wasmGet", ref_of(recv), prop.to_s))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def set(recv, prop, value)
|
|
55
|
+
@backend.call_js("__rbHost.wasmSet", ref_of(recv), prop.to_s, pack(value))
|
|
56
|
+
nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def call(recv, method, args)
|
|
60
|
+
unpack(@backend.call_js("__rbHost.wasmCall", ref_of(recv), method.to_s, args.map { |a| pack(a) }))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Apply a function ref directly (optionally with an explicit `this`).
|
|
64
|
+
def apply(fn, this_arg, args)
|
|
65
|
+
this_ref = this_arg.nil? ? nil : ref_of(this_arg)
|
|
66
|
+
unpack(@backend.call_js("__rbHost.wasmApply", ref_of(fn), this_ref, args.map { |a| pack(a) }))
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def construct(ctor, args)
|
|
70
|
+
unpack(@backend.call_js("__rbHost.wasmNew", ref_of(ctor), args.map { |a| pack(a) }))
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def typeof(value)
|
|
74
|
+
@backend.call_js("__rbHost.wasmTypeof", ref_of(value))
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def to_string(value)
|
|
78
|
+
@backend.call_js("__rbHost.wasmToString", ref_of(value))
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def strict_equal(a, b)
|
|
82
|
+
@backend.call_js("__rbHost.wasmStrictEqual", ref_of(a), ref_of(b))
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def js_null?(value)
|
|
86
|
+
return value.nil? unless value.is_a?(JSValue)
|
|
87
|
+
|
|
88
|
+
@backend.call_js("__rbHost.wasmIsNull", value.ref)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def instance_of?(value, ctor)
|
|
92
|
+
@backend.call_js("__rbHost.wasmInstanceof", ref_of(value), ref_of(ctor))
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Make a JS function (returned as a ref) that calls back into the guest
|
|
96
|
+
# with the given invoke-id when invoked.
|
|
97
|
+
def make_callback(invoke_id)
|
|
98
|
+
unpack(@backend.call_js("__rbHost.wasmMakeCallback", invoke_id.to_i))
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def release(value)
|
|
102
|
+
@backend.call_js("__rbHost.wasmReleaseRef", value.ref) if value.is_a?(JSValue)
|
|
103
|
+
nil
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Ruby value -> wasm-tagged JS value. Public so the embedder's
|
|
107
|
+
# #on_invoke dispatcher can pack the values it hands back into JS.
|
|
108
|
+
def pack(value)
|
|
109
|
+
case value
|
|
110
|
+
when JSValue then {"__rb_js_ref" => value.ref}
|
|
111
|
+
when nil, true, false, Integer, Float, String then value
|
|
112
|
+
when Symbol then value.to_s
|
|
113
|
+
when Array then value.map { |e| pack(e) }
|
|
114
|
+
when Hash then value.each_with_object({}) { |(k, v), h| h[k.to_s] = pack(v) }
|
|
115
|
+
else
|
|
116
|
+
raise ArgumentError, "cannot pack #{value.class} for the wasm JS bridge"
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# wasm-tagged JS value -> Ruby value (JSValue for refs). Public for the
|
|
121
|
+
# same reason as #pack.
|
|
122
|
+
def unpack(value)
|
|
123
|
+
case value
|
|
124
|
+
when Hash
|
|
125
|
+
if value.key?("__rb_js_ref")
|
|
126
|
+
JSValue.new(value["__rb_js_ref"])
|
|
127
|
+
elsif value.key?("__rb_undefined")
|
|
128
|
+
nil
|
|
129
|
+
elsif value.key?("__rb_bytes")
|
|
130
|
+
value["__rb_bytes"]
|
|
131
|
+
elsif value.key?("__rb_arraybuffer")
|
|
132
|
+
value["__rb_arraybuffer"]
|
|
133
|
+
else
|
|
134
|
+
value.each_with_object({}) { |(k, v), h| h[k] = unpack(v) }
|
|
135
|
+
end
|
|
136
|
+
when Array then value.map { |e| unpack(e) }
|
|
137
|
+
else value
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
private
|
|
142
|
+
|
|
143
|
+
def ref_of(value)
|
|
144
|
+
return value.ref if value.is_a?(JSValue)
|
|
145
|
+
|
|
146
|
+
raise ArgumentError, "expected a JSValue receiver, got #{value.inspect}"
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "quickjs/version"
|
|
4
|
+
|
|
5
|
+
module Dommy
|
|
6
|
+
module Js
|
|
7
|
+
module Quickjs
|
|
8
|
+
class Error < StandardError; end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
require_relative "handle_table"
|
|
14
|
+
require_relative "dom_interfaces"
|
|
15
|
+
require_relative "constructor_registry"
|
|
16
|
+
require_relative "custom_elements"
|
|
17
|
+
require_relative "host_bridge"
|
|
18
|
+
require_relative "quickjs/backend"
|
|
19
|
+
require_relative "quickjs/wasm_bridge"
|
|
20
|
+
require_relative "quickjs/runtime"
|
metadata
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: dommy-js-quickjs
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- takahashim
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 2026-05-31 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: quickjs
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 0.18.0
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 0.18.0
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: dommy
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: 0.8.1
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: 0.8.1
|
|
40
|
+
description: |
|
|
41
|
+
dommy-js-quickjs lets JavaScript drive a Dommy DOM by embedding QuickJS (via
|
|
42
|
+
the quickjs gem) and bridging DOM nodes to JS through an ES Proxy that routes
|
|
43
|
+
property/method access into Dommy's __js_get__ / __js_set__ / __js_call__ ABI.
|
|
44
|
+
email:
|
|
45
|
+
- takahashimm@gmail.com
|
|
46
|
+
executables: []
|
|
47
|
+
extensions: []
|
|
48
|
+
extra_rdoc_files: []
|
|
49
|
+
files:
|
|
50
|
+
- CHANGELOG.md
|
|
51
|
+
- LICENSE.txt
|
|
52
|
+
- README.md
|
|
53
|
+
- Rakefile
|
|
54
|
+
- docs/bridge-redesign.md
|
|
55
|
+
- docs/wpt-conformance.md
|
|
56
|
+
- lib/dommy/js/constructor_registry.rb
|
|
57
|
+
- lib/dommy/js/custom_elements.rb
|
|
58
|
+
- lib/dommy/js/dom_interfaces.rb
|
|
59
|
+
- lib/dommy/js/handle_table.rb
|
|
60
|
+
- lib/dommy/js/host_bridge.rb
|
|
61
|
+
- lib/dommy/js/host_runtime.js
|
|
62
|
+
- lib/dommy/js/observable_runtime.js
|
|
63
|
+
- lib/dommy/js/quickjs.rb
|
|
64
|
+
- lib/dommy/js/quickjs/backend.rb
|
|
65
|
+
- lib/dommy/js/quickjs/capybara.rb
|
|
66
|
+
- lib/dommy/js/quickjs/runtime.rb
|
|
67
|
+
- lib/dommy/js/quickjs/version.rb
|
|
68
|
+
- lib/dommy/js/quickjs/wasm_bridge.rb
|
|
69
|
+
- sig/dommy/js/quickjs.rbs
|
|
70
|
+
homepage: https://github.com/takahashim/dommy-js-quickjs
|
|
71
|
+
licenses:
|
|
72
|
+
- MIT
|
|
73
|
+
metadata:
|
|
74
|
+
homepage_uri: https://github.com/takahashim/dommy-js-quickjs
|
|
75
|
+
source_code_uri: https://github.com/takahashim/dommy-js-quickjs
|
|
76
|
+
bug_tracker_uri: https://github.com/takahashim/dommy-js-quickjs/issues
|
|
77
|
+
rubygems_mfa_required: 'true'
|
|
78
|
+
rdoc_options: []
|
|
79
|
+
require_paths:
|
|
80
|
+
- lib
|
|
81
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
82
|
+
requirements:
|
|
83
|
+
- - ">="
|
|
84
|
+
- !ruby/object:Gem::Version
|
|
85
|
+
version: 3.2.0
|
|
86
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
87
|
+
requirements:
|
|
88
|
+
- - ">="
|
|
89
|
+
- !ruby/object:Gem::Version
|
|
90
|
+
version: '0'
|
|
91
|
+
requirements: []
|
|
92
|
+
rubygems_version: 3.6.2
|
|
93
|
+
specification_version: 4
|
|
94
|
+
summary: QuickJS backend for running JavaScript against a Dommy DOM.
|
|
95
|
+
test_files: []
|