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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 17fc429b996f9fb344e581d754fa51c5a0e27d559caca8709fdd1dd6bcbd8516
4
+ data.tar.gz: de228d74c5b14116c3eae917154823c00f0cd3c5f3c81a89d4b33d1c89c6b44b
5
+ SHA512:
6
+ metadata.gz: a4340bfcd9e6c084ee9441bafc176752bb97601814d460fd39cac699553faf52c972d08f844011a957c90b0df7b4c83b609df618f10d9545a58e4d22bb314f0e
7
+ data.tar.gz: 628bb3957b81036927500ee813d6b0410bc936ac378e35efc27990f8cc59c6d5ed4eb9b18bd9e1baed781bb813a7d09d864c1bf74e40d50b03cac588e0d5ec3a
data/CHANGELOG.md ADDED
@@ -0,0 +1,6 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ initial release.
6
+
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Masayoshi Takahashi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,150 @@
1
+ # Dommy::Js::Quickjs
2
+
3
+ Run real JavaScript — including real frontend frameworks — against a
4
+ [Dommy](https://github.com/takahashim/dommy) DOM, without a browser.
5
+
6
+ JavaScript executes in an embedded QuickJS VM (via the
7
+ [`quickjs`](https://github.com/hmsk/quickjs.rb) gem). Dommy DOM nodes are bridged
8
+ to JS as objects whose property/method access routes into Dommy's
9
+ `__js_get__` / `__js_set__` / `__js_call__` / `__js_new__` ABI, so JS drives a
10
+ real Dommy document. The QuickJS-specific code is isolated in a small `Backend`;
11
+ the rest of the bridge is engine-agnostic.
12
+
13
+ The bridge presents a **spec-shaped JS DOM**, not bare proxies: `instanceof`,
14
+ prototype chains, `Object.prototype.toString` brands, constructable interfaces
15
+ (`new Event(...)`), custom elements (`class extends HTMLElement`), live
16
+ collections, and expandos all work — enough that the real
17
+ [`@hotwired/turbo`](https://github.com/hotwired/turbo) bundle loads and drives
18
+ the DOM (turbo-stream + turbo-frame; see `test/dommy/js/test_turbo_integration.rb`).
19
+
20
+ ## Installation
21
+
22
+ In your `Gemfile`:
23
+
24
+ ```ruby
25
+ gem "dommy-js-quickjs"
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ ```ruby
31
+ require "dommy"
32
+ require "dommy/js/quickjs"
33
+
34
+ win = Dommy.parse("<h1 class='title'>Hi</h1>")
35
+
36
+ rt = Dommy::Js::Quickjs::Runtime.new
37
+ rt.define_host_object("document", win.document)
38
+ rt.install_window(win) # exposes `window` + bare timer globals (setTimeout, ...)
39
+
40
+ rt.evaluate('document.querySelector(".title").textContent') # => "Hi"
41
+ rt.execute('document.querySelector(".title").textContent = "Bye"') # mutates the DOM
42
+ win.document.query_selector(".title").text_content # => "Bye"
43
+ ```
44
+
45
+ - `evaluate(js)` — evaluate an expression (or a `return`-using statement body) and
46
+ return the value; DOM nodes come back as Dommy objects, Promises are awaited.
47
+ - `execute(js)` — run statements for side effects; drains microtasks.
48
+ - Timers ride Dommy's scheduler: `win.scheduler.advance_time(ms)` fires JS
49
+ `setTimeout` / `setInterval` / `requestAnimationFrame` callbacks.
50
+ - `run_until_idle` — drive the event loop to quiescence: drains microtasks, then
51
+ advances the scheduler to each due timer and drains again, in WHATWG order
52
+ (microtasks before each timer), until nothing is pending. The one-call
53
+ "settle everything" entry point after an eval (`max_iterations:` bounds
54
+ self-rescheduling timer loops).
55
+
56
+ ## What the JS sees
57
+
58
+ The JS-facing DOM behaves like a browser's, not like a plain object graph:
59
+
60
+ ```js
61
+ const el = document.querySelector("button");
62
+ el instanceof HTMLElement // true (full prototype chain)
63
+ Object.prototype.toString.call(el) // "[object HTMLButtonElement]"
64
+ el.constructor.name // "HTMLButtonElement"
65
+
66
+ new CustomEvent("x", { detail: 1 }) // constructable interfaces
67
+ el.dispatchEvent(new CustomEvent("x", {...})) // events bubble; detail round-trips
68
+
69
+ for (const c of el.children) { /* ... */ } // live, iterable collections
70
+ el.querySelectorAll("li").map(n => n.tagName) // NodeList crosses as a JS array
71
+
72
+ el._state = { n: 1 }; el._state === el._state // expandos keep their identity
73
+
74
+ class Card extends HTMLElement { // custom elements
75
+ connectedCallback() { this.textContent = "hi"; }
76
+ }
77
+ customElements.define("x-card", Card);
78
+ ```
79
+
80
+ Method-vs-property is taken from each Dommy class's `__js_method_names__`, so no
81
+ method list is maintained here.
82
+
83
+ ## Running real frameworks (and testing components)
84
+
85
+ To run a real bundle you need the browser globals it reaches for and a way to
86
+ drive deferred work. `BrowserHarness` (under `test/support/`) packages this:
87
+
88
+ ```ruby
89
+ h = Dommy::Js::BrowserHarness.new(
90
+ "<body><div id='app'></div></body>",
91
+ fetch_stub: { "http://localhost/frame" => { status: 200, contentType: "text/html", body: "..." } }
92
+ )
93
+ h.load_script("vendor/turbo.umd.js") # runs the real bundle
94
+ h.execute("/* your app / interactions */")
95
+ h.pump # drive microtasks + the scheduler clock
96
+ assert_empty h.errors # nothing was silently swallowed
97
+ ```
98
+
99
+ The pieces it relies on are public Runtime API:
100
+
101
+ - `Runtime#install_browser_globals` — wire the bare globals real bundles use
102
+ (`self` / `location` / `history` / `navigator` / storages / `CSS` / `fetch` /
103
+ `addEventListener` / …), aliased onto the installed window.
104
+ - `Runtime#on_unhandled_rejection { |err| }` — surface promise rejections that
105
+ reach the microtask queue with no handler. Frameworks swallow these; `err.backtrace`
106
+ carries the JS stack, which is the difference between blind and one-shot debugging.
107
+ - `Runtime#on_log { |log| }` — observe `console.*` (`log.severity` / `log.to_s`).
108
+
109
+ ### Capybara
110
+
111
+ Requiring the adapter enables `execute_script` / `evaluate_script` on
112
+ `Capybara::Dommy::Driver` (capybara-dommy stays JS-free without it):
113
+
114
+ ```ruby
115
+ require "dommy/js/quickjs/capybara"
116
+ ```
117
+
118
+ ## Limitations
119
+
120
+ - **Deterministic scheduler, no wall clock.** Async work (timers, `requestAnimationFrame`,
121
+ framework "next repaint" deferral) only advances via `win.scheduler.advance_time(ms)` —
122
+ drive it with `Runtime#run_until_idle`, `BrowserHarness#pump`, or manually.
123
+ Selenium-style `done()` / real-time waits are not supported.
124
+ - **`fetch` is stub-based** via Dommy's `__fetchy_stub__` (a `{ url => entry }` map);
125
+ there is no real network.
126
+ - **Event listeners must be functions** — `addEventListener("...", fn)` works
127
+ (closures intact; `removeEventListener` with the same function detaches it), but
128
+ the `{ handleEvent }` object form is not.
129
+ - **Expandos are scoped to elements**, and the JS callback table is not evicted, so
130
+ a very long-lived VM can grow unbounded.
131
+ - **DOM coverage is Dommy's.** A JS method/property works only where Dommy exposes
132
+ it via the ABI (`__js_method_names__` / `__js_get__`); gaps surface as
133
+ `undefined` / "not a function" (see `on_unhandled_rejection`).
134
+
135
+ ## Development
136
+
137
+ Run `bin/setup` to install dependencies, then `rake test`. `bin/console` opens an
138
+ interactive prompt.
139
+
140
+ The real-framework integration test (`test_turbo_integration.rb`) is skipped
141
+ unless the Turbo bundle is vendored at `test/fixtures/turbo.umd.js`:
142
+
143
+ ```bash
144
+ curl -sL https://unpkg.com/@hotwired/turbo@8/dist/turbo.es2017-umd.js \
145
+ -o test/fixtures/turbo.umd.js
146
+ ```
147
+
148
+ ## Contributing
149
+
150
+ Bug reports and pull requests are welcome at https://github.com/takahashim/dommy-js-quickjs.
data/Rakefile ADDED
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ task default: :test
9
+
10
+ namespace :wpt do
11
+ desc "Run the vendored WPT corpus against the bridge and report a conformance rate"
12
+ task :conformance, [:filter] do |_t, args|
13
+ $LOAD_PATH.unshift File.expand_path("test", __dir__)
14
+ require "test_helper"
15
+ require "support/wpt_runner"
16
+
17
+ runner = Dommy::Js::WptRunner
18
+ files = runner.manifest
19
+ files = files.grep(/#{args[:filter]}/) if args[:filter]
20
+
21
+ total_pass = total = file_ok = 0
22
+ blocked = []
23
+ by_dir = Hash.new { |h, k| h[k] = [0, 0] } # dir => [pass, total]
24
+ files.each do |rel|
25
+ results = runner.run(rel)
26
+ pass = results.count(&:pass?)
27
+ n = results.size
28
+ total_pass += pass
29
+ total += n
30
+ dir = rel.split("/").first
31
+ by_dir[dir][0] += pass
32
+ by_dir[dir][1] += n
33
+ file_ok += 1 if n.positive? && pass == n
34
+ flag = n.zero? ? "—" : (pass == n ? "✓" : " ")
35
+ printf(" %s %-46s %3d/%-3d\n", flag, rel, pass, n)
36
+ results.reject(&:pass?).first(3).each { |r| puts " #{r.to_s[0, 110]}" }
37
+ rescue => e
38
+ blocked << rel
39
+ puts " ✗ #{rel} (errored: #{e.class}: #{e.message[0, 80]})"
40
+ end
41
+
42
+ pct = ->(p, t) { t.zero? ? 0 : (100.0 * p / t).round(1) }
43
+ puts
44
+ by_dir.sort.each { |dir, (p, t)| printf(" %-8s %4d/%-5d (%.1f%%)\n", dir, p, t, pct.call(p, t)) }
45
+ puts
46
+ puts "WPT conformance: #{total_pass}/#{total} subtests (#{pct.call(total_pass, total)}%) across #{files.size} files; " \
47
+ "#{file_ok} files fully green#{blocked.empty? ? "" : ", #{blocked.size} errored"}"
48
+ end
49
+ end