dommy-js-quickjs 0.1.0 → 0.9.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.
@@ -0,0 +1,304 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dommy
4
+ module Js
5
+ module Quickjs
6
+ # Source-level workaround for a QuickJS bytecode-generation bug: a `for...of`
7
+ # whose ITERABLE expression contains a `yield` fails to COMPILE with an
8
+ # internal "stack underflow" error (the for-of iterator-close finally region
9
+ # miscomputes the operand stack across the generator suspend). V8 / real
10
+ # browsers compile it fine, so SPA bundles ship it — e.g. note.com's modern
11
+ # build has `for (var f of (yield O(), _)) v(f)`, and the whole code-split
12
+ # chunk fails to load, so the app never mounts.
13
+ #
14
+ # The fix hoists the iterable into a temp `var` so the `yield` leaves the
15
+ # for-of operand position — `for (x of (yield a, b)) …` becomes
16
+ # `var t = (yield a, b); for (x of t) …` — semantically identical, and it
17
+ # compiles. Applied ONLY as a retry after a genuine "stack underflow" compile
18
+ # failure (see Backend), so working scripts are never rewritten and an
19
+ # imperfect transform cannot regress anything (the source already failed).
20
+ module SourceGuard
21
+ module_function
22
+
23
+ # The marker QuickJS raises for this codegen bug.
24
+ ERROR_MARKER = "stack underflow"
25
+
26
+ def relevant_error?(error)
27
+ error.respond_to?(:message) && error.message.to_s.include?(ERROR_MARKER)
28
+ end
29
+
30
+ # Rewrite every `for...of` whose iterable contains a `yield`, hoisting the
31
+ # iterable into a preceding `var`. Returns the source unchanged when there
32
+ # is nothing to do.
33
+ def fix_for_of_yield(source)
34
+ return source unless source.include?("yield")
35
+
36
+ # Scan at the BYTE level (binary encoding): on a multibyte (UTF-8) source
37
+ # — note's bundle has Japanese strings — Ruby's character indexing is
38
+ # O(index), making a char-by-char pass O(n^2). All tokens we look for are
39
+ # ASCII, and UTF-8 continuation bytes (>= 0x80) never collide with them,
40
+ # so byte scanning is both safe and O(1) per position.
41
+ Rewriter.new(source.b).run.force_encoding(source.encoding)
42
+ end
43
+
44
+ IDENT = /[A-Za-z0-9_$]/.freeze
45
+
46
+ def ident_char?(ch)
47
+ !ch.nil? && IDENT.match?(ch)
48
+ end
49
+
50
+ # A `/` begins a regex (not division) unless the previous significant code
51
+ # char can end an expression (ident / `)` / `]` / a literal).
52
+ def regex_allowed?(prev_sig)
53
+ return true if prev_sig.nil?
54
+ return false if ident_char?(prev_sig)
55
+
56
+ !")]'\"`".include?(prev_sig)
57
+ end
58
+
59
+ # If `src[i]` begins a string, template, regex literal, or comment, return
60
+ # the index just after it; otherwise nil. `prev_sig` (previous significant
61
+ # code char) disambiguates a regex `/` from division.
62
+ def atom_end(src, i, prev_sig)
63
+ case src[i]
64
+ when "/"
65
+ if src[i + 1] == "/"
66
+ src.index("\n", i) || src.length
67
+ elsif src[i + 1] == "*"
68
+ (idx = src.index("*/", i + 2)) ? idx + 2 : src.length
69
+ elsif regex_allowed?(prev_sig)
70
+ scan_regex(src, i)
71
+ end
72
+ when "'", '"'
73
+ scan_quote(src, i, src[i])
74
+ when "`"
75
+ scan_template(src, i)
76
+ end
77
+ end
78
+
79
+ def scan_quote(src, i, quote)
80
+ j = i + 1
81
+ n = src.length
82
+ while j < n
83
+ c = src[j]
84
+ return j + 1 if c == quote
85
+
86
+ j += c == "\\" ? 2 : 1
87
+ end
88
+ n
89
+ end
90
+
91
+ def scan_template(src, i)
92
+ j = i + 1
93
+ n = src.length
94
+ while j < n
95
+ c = src[j]
96
+ return j + 1 if c == "`"
97
+ if c == "\\"
98
+ j += 2
99
+ next
100
+ end
101
+ if c == "$" && src[j + 1] == "{"
102
+ close = match_bracket(src, j + 1)
103
+ return n unless close
104
+
105
+ j = close + 1
106
+ next
107
+ end
108
+ j += 1
109
+ end
110
+ n
111
+ end
112
+
113
+ # End of a regex literal at `i` (past its flags), or nil if it isn't one
114
+ # (an unescaped newline before the closing `/` → it was division).
115
+ def scan_regex(src, i)
116
+ j = i + 1
117
+ n = src.length
118
+ in_class = false
119
+ while j < n
120
+ c = src[j]
121
+ case c
122
+ when "\\" then j += 2; next
123
+ when "[" then in_class = true
124
+ when "]" then in_class = false
125
+ when "/"
126
+ unless in_class
127
+ j += 1
128
+ j += 1 while j < n && src[j] =~ /[a-z]/i
129
+ return j
130
+ end
131
+ when "\n"
132
+ return nil
133
+ end
134
+ j += 1
135
+ end
136
+ nil
137
+ end
138
+
139
+ # Index of the close bracket matching the (/[/{ at `open`, skipping nested
140
+ # brackets, strings, templates, regexes and comments. `limit` bounds the
141
+ # scan (a for-head is short; bounding keeps the whole pass linear and
142
+ # avoids a runaway scan when a mis-lexed token unbalances brackets).
143
+ def match_bracket(src, open, limit: nil)
144
+ pairs = {"(" => ")", "[" => "]", "{" => "}"}
145
+ want = [pairs[src[open]]]
146
+ i = open + 1
147
+ n = src.length
148
+ n = [n, open + limit].min if limit
149
+ prev = src[open]
150
+ while i < n
151
+ stop = atom_end(src, i, prev)
152
+ if stop
153
+ prev = src[stop - 1]
154
+ i = stop
155
+ next
156
+ end
157
+ c = src[i]
158
+ if pairs.key?(c)
159
+ want << pairs[c]
160
+ elsif ")]}".include?(c)
161
+ return i if want.size == 1 && want.last == c
162
+
163
+ want.pop if want.last == c
164
+ end
165
+ prev = c unless c =~ /\s/
166
+ i += 1
167
+ end
168
+ nil
169
+ end
170
+
171
+ # Position of the for-of `of` keyword at the top level of a for-head, or nil
172
+ # for a for-in / C-style for (a top-level `;` rules out for-of).
173
+ def top_level_of(head)
174
+ depth = 0
175
+ i = 0
176
+ n = head.length
177
+ prev = nil
178
+ while i < n
179
+ stop = atom_end(head, i, prev)
180
+ if stop
181
+ prev = head[stop - 1]
182
+ i = stop
183
+ next
184
+ end
185
+ c = head[i]
186
+ case c
187
+ when "(", "[", "{" then depth += 1
188
+ when ")", "]", "}" then depth -= 1
189
+ when ";" then return nil if depth.zero?
190
+ when "o"
191
+ if depth.zero? && head[i, 2] == "of" &&
192
+ !ident_char?(i.zero? ? nil : head[i - 1]) && !ident_char?(head[i + 2])
193
+ return i
194
+ end
195
+ end
196
+ prev = c unless c =~ /\s/
197
+ i += 1
198
+ end
199
+ nil
200
+ end
201
+
202
+ def contains_yield?(expr)
203
+ i = 0
204
+ n = expr.length
205
+ prev = nil
206
+ while i < n
207
+ stop = atom_end(expr, i, prev)
208
+ if stop
209
+ prev = expr[stop - 1]
210
+ i = stop
211
+ next
212
+ end
213
+ if expr[i] == "y" && expr[i, 5] == "yield" &&
214
+ !ident_char?(i.zero? ? nil : expr[i - 1]) && !ident_char?(expr[i + 5])
215
+ return true
216
+ end
217
+ prev = expr[i] unless expr[i] =~ /\s/
218
+ i += 1
219
+ end
220
+ false
221
+ end
222
+
223
+ # Walks source once, copying literals/comments verbatim and rewriting a
224
+ # for-of-with-yield-in-iterable found at a statement boundary.
225
+ class Rewriter
226
+ # Upper bound on a `for (...)` head length we will consider (well above
227
+ # any real for-of header, including a large iterable expression).
228
+ HEAD_SCAN_LIMIT = 64 * 1024
229
+
230
+ def initialize(src)
231
+ @src = src
232
+ @n = src.length
233
+ end
234
+
235
+ def run
236
+ out = +""
237
+ i = 0
238
+ prev = nil # previous significant code char
239
+ counter = 0
240
+ while i < @n
241
+ stop = SourceGuard.atom_end(@src, i, prev)
242
+ if stop
243
+ out << @src[i...stop]
244
+ prev = @src[stop - 1]
245
+ i = stop
246
+ next
247
+ end
248
+
249
+ ch = @src[i]
250
+ if ch == "f" && @src[i, 3] == "for" &&
251
+ !SourceGuard.ident_char?(i.zero? ? nil : @src[i - 1]) &&
252
+ !SourceGuard.ident_char?(@src[i + 3]) &&
253
+ boundary?(prev)
254
+ rewrite = transform_for(i, counter)
255
+ if rewrite
256
+ out << rewrite[:text]
257
+ i = rewrite[:next]
258
+ counter += 1
259
+ prev = ")"
260
+ next
261
+ end
262
+ end
263
+
264
+ out << ch
265
+ prev = ch unless ch =~ /\s/
266
+ i += 1
267
+ end
268
+ out
269
+ end
270
+
271
+ private
272
+
273
+ # A `var t = …;` may be inserted before `for` only at a statement
274
+ # boundary; otherwise (e.g. `if (c) for (…)`) it would change meaning.
275
+ def boundary?(prev_sig)
276
+ prev_sig.nil? || ";{}".include?(prev_sig)
277
+ end
278
+
279
+ def transform_for(for_idx, counter)
280
+ open = for_idx + 3
281
+ open += 1 while open < @n && @src[open] =~ /\s/
282
+ return nil unless @src[open] == "("
283
+
284
+ # A for-head is short; cap the scan so an unbalanced (mis-lexed) head
285
+ # can't turn the per-`for` work into an O(n) scan.
286
+ close = SourceGuard.match_bracket(@src, open, limit: HEAD_SCAN_LIMIT)
287
+ return nil unless close
288
+
289
+ head = @src[(open + 1)...close]
290
+ of_at = SourceGuard.top_level_of(head)
291
+ return nil unless of_at
292
+
293
+ decl = head[0...of_at]
294
+ iterable = head[(of_at + 2)..].to_s
295
+ return nil unless SourceGuard.contains_yield?(iterable)
296
+
297
+ tmp = "__dommyForOf#{counter}"
298
+ {text: "var #{tmp}=(#{iterable});for(#{decl}of #{tmp})", next: close + 1}
299
+ end
300
+ end
301
+ end
302
+ end
303
+ end
304
+ end
@@ -3,7 +3,7 @@
3
3
  module Dommy
4
4
  module Js
5
5
  module Quickjs
6
- VERSION = "0.1.0"
6
+ VERSION = "0.9.0"
7
7
  end
8
8
  end
9
9
  end
@@ -15,13 +15,13 @@ module Dommy
15
15
  # cross as plain Ruby values. Callbacks the guest registers become JS
16
16
  # functions (also refs) that route back through `__rbWasmInvoke`.
17
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
18
+ # An opaque handle to a JS value living in the VM, identified by its id
19
+ # into the JS-side `jsRefs` table. This is the SAME table and the SAME
20
+ # `__rb_js_ref` tag the Proxy bridge's dehydrate/rehydrate use (see
21
+ # host_runtime.js), so a ref is one concept across both bridges — hence a
22
+ # single shared type. Aliased here so existing `WasmBridge::JSValue`
23
+ # callers keep resolving.
24
+ JSValue = ::Dommy::Bridge::JSValue
25
25
 
26
26
  def initialize(backend)
27
27
  @backend = backend
@@ -107,7 +107,7 @@ module Dommy
107
107
  # #on_invoke dispatcher can pack the values it hands back into JS.
108
108
  def pack(value)
109
109
  case value
110
- when JSValue then {"__rb_js_ref" => value.ref}
110
+ when JSValue then {WireTags::JS_REF => value.ref}
111
111
  when nil, true, false, Integer, Float, String then value
112
112
  when Symbol then value.to_s
113
113
  when Array then value.map { |e| pack(e) }
@@ -122,14 +122,14 @@ module Dommy
122
122
  def unpack(value)
123
123
  case value
124
124
  when Hash
125
- if value.key?("__rb_js_ref")
126
- JSValue.new(value["__rb_js_ref"])
127
- elsif value.key?("__rb_undefined")
125
+ if value.key?(WireTags::JS_REF)
126
+ JSValue.new(value[WireTags::JS_REF])
127
+ elsif value.key?(WireTags::UNDEFINED)
128
128
  nil
129
- elsif value.key?("__rb_bytes")
130
- value["__rb_bytes"]
131
- elsif value.key?("__rb_arraybuffer")
132
- value["__rb_arraybuffer"]
129
+ elsif value.key?(WireTags::BYTES)
130
+ value[WireTags::BYTES]
131
+ elsif value.key?(WireTags::ARRAY_BUFFER)
132
+ value[WireTags::ARRAY_BUFFER]
133
133
  else
134
134
  value.each_with_object({}) { |(k, v), h| h[k] = unpack(v) }
135
135
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "dommy"
3
4
  require_relative "quickjs/version"
4
5
 
5
6
  module Dommy
@@ -10,11 +11,18 @@ module Dommy
10
11
  end
11
12
  end
12
13
 
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"
14
+ # The engine-agnostic host layer lives in the `dommy` gem (loaded above via
15
+ # `require "dommy"`): the Runtime port + registry, ScriptBoot/ImportMap/
16
+ # ModuleLoader, Dommy::Browser, AND the JS<->Ruby DOM bridge (HostBridge +
17
+ # WireTags / HandleTable / DomInterfaces / ConstructorResolver /
18
+ # CustomElementBridge, with host_runtime.js / observable_runtime.js). This gem
19
+ # provides only the QuickJS backend that plugs in underneath.
18
20
  require_relative "quickjs/backend"
19
21
  require_relative "quickjs/wasm_bridge"
20
22
  require_relative "quickjs/runtime"
23
+ require_relative "quickjs/script_cache"
24
+
25
+ # Register QuickJS as a pluggable JS runtime backend (the default). The host
26
+ # layer builds runtimes through `Dommy::Js.build_runtime` rather than naming
27
+ # this class directly.
28
+ Dommy::Js.register_runtime(:quickjs) { |**opts| Dommy::Js::Quickjs::Runtime.new(**opts) }
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env bash
2
+ # Regenerate test/fixtures/jsx-transform.umd.js — a minimal JSX→JS transpiler
3
+ # (sucrase) bundled into a single IIFE that exposes `globalThis.transformJSX`.
4
+ #
5
+ # Sucrase is a tiny, JSX/TS-focused alternative to Babel: the bundle is ~200 KB
6
+ # vs @babel/standalone's ~3 MB, since it carries only the JSX transform. At run
7
+ # time the bundle is loaded into the QuickJS VM (pure Ruby + RubyGems — no Node,
8
+ # no native binary); Node/esbuild are only needed here to (re)build it.
9
+ #
10
+ # script/build_jsx_transform.sh
11
+ set -euo pipefail
12
+
13
+ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
14
+ OUT="$ROOT/test/fixtures/jsx-transform.umd.js"
15
+ TMP="$(mktemp -d)"
16
+ trap 'rm -rf "$TMP"' EXIT
17
+
18
+ cd "$TMP"
19
+ npm init -y >/dev/null 2>&1
20
+ npm install sucrase >/dev/null 2>&1
21
+ # `production: true` emits classic React.createElement calls (no dev __source/
22
+ # __self props), which the vendored React UMD build consumes.
23
+ echo "import { transform } from 'sucrase';
24
+ globalThis.transformJSX = (code) => transform(code, { transforms: ['jsx'], production: true }).code;" > entry.js
25
+
26
+ npx --yes esbuild@0.24.0 entry.js --bundle --format=iife --minify --outfile="$OUT"
27
+ echo "Wrote $OUT ($(wc -c < "$OUT") bytes)"
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env bash
2
+ # Regenerate test/fixtures/stimulus-tests.umd.js — the @hotwired/stimulus QUnit
3
+ # suite bundled into a single IIFE script runnable in the QuickJS VM.
4
+ #
5
+ # Requirements: git, node + npx (esbuild is fetched on demand).
6
+ #
7
+ # script/build_stimulus_tests.sh [stimulus-git-ref]
8
+ #
9
+ # Stimulus runs its tests under Karma+webpack; we replicate the inputs:
10
+ # 1. Clone the source (its tests live in src/tests, not the npm package).
11
+ # 2. Replace the webpack-only `require.context` entry with an explicit entry
12
+ # that imports every *_tests.ts module and calls defineModule().
13
+ # 3. Patch TestCase.testPropertyNames to use getOwnPropertyNames instead of
14
+ # Object.keys — native ES2017 class methods are non-enumerable, whereas
15
+ # Stimulus's own es5 (tsc) build emits enumerable prototype assignments.
16
+ # The two are equivalent (own, string-keyed); this just lets the esbuild
17
+ # ES2017 bundle discover the `test ...` methods.
18
+ # 4. esbuild-bundle to an IIFE that references a global QUnit (the shim in
19
+ # test/support/qunit_shim.js provides it at run time).
20
+ set -euo pipefail
21
+
22
+ REF="${1:-v3.2.2}"
23
+ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
24
+ OUT="$ROOT/test/fixtures/stimulus-tests.umd.js"
25
+ TMP="$(mktemp -d)"
26
+ trap 'rm -rf "$TMP"' EXIT
27
+
28
+ echo "Cloning hotwired/stimulus @ $REF ..."
29
+ git clone --depth 1 --branch "$REF" -q https://github.com/hotwired/stimulus.git "$TMP/stimulus"
30
+ cd "$TMP/stimulus"
31
+
32
+ # (3) enumerability patch
33
+ ruby -i -pe 'sub(/Object\.keys\(this\.prototype\)/, "Object.getOwnPropertyNames(this.prototype)")' \
34
+ src/tests/cases/test_case.ts
35
+
36
+ # (2) explicit entry replacing require.context
37
+ ruby -e '
38
+ mods = Dir.chdir("src/tests") { Dir["modules/**/*_tests.ts"].sort }
39
+ imports = mods.each_with_index.map { |m, i| %(import M#{i} from "./#{m.sub(/\.ts$/, "")}") }
40
+ File.write("src/tests/conformance.entry.ts",
41
+ imports.join("\n") + "\n" +
42
+ "const MODULES = [#{mods.size.times.map { |i| "M#{i}" }.join(", ")}]\n" +
43
+ "MODULES.forEach((c) => c.defineModule())\n")
44
+ puts "entry: #{mods.size} modules"
45
+ '
46
+
47
+ # (4) bundle
48
+ npx --yes esbuild@0.24.0 src/tests/conformance.entry.ts \
49
+ --bundle --format=iife --target=es2017 --tsconfig=tsconfig.json \
50
+ --outfile="$OUT"
51
+
52
+ echo "Wrote $OUT"
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dommy-js-quickjs
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - takahashim
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-05-31 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: quickjs
@@ -27,16 +27,16 @@ dependencies:
27
27
  name: dommy
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
- - - "~>"
30
+ - - ">="
31
31
  - !ruby/object:Gem::Version
32
- version: 0.8.1
32
+ version: 0.9.0
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
- - - "~>"
37
+ - - ">="
38
38
  - !ruby/object:Gem::Version
39
- version: 0.8.1
39
+ version: 0.9.0
40
40
  description: |
41
41
  dommy-js-quickjs lets JavaScript drive a Dommy DOM by embedding QuickJS (via
42
42
  the quickjs gem) and bridging DOM nodes to JS through an ES Proxy that routes
@@ -47,25 +47,24 @@ executables: []
47
47
  extensions: []
48
48
  extra_rdoc_files: []
49
49
  files:
50
+ - ".github/workflows/test.yml"
50
51
  - CHANGELOG.md
51
52
  - LICENSE.txt
52
53
  - README.md
53
54
  - Rakefile
54
55
  - docs/bridge-redesign.md
55
56
  - 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
57
  - lib/dommy/js/quickjs.rb
64
58
  - lib/dommy/js/quickjs/backend.rb
65
59
  - lib/dommy/js/quickjs/capybara.rb
60
+ - lib/dommy/js/quickjs/rack.rb
66
61
  - lib/dommy/js/quickjs/runtime.rb
62
+ - lib/dommy/js/quickjs/script_cache.rb
63
+ - lib/dommy/js/quickjs/source_guard.rb
67
64
  - lib/dommy/js/quickjs/version.rb
68
65
  - lib/dommy/js/quickjs/wasm_bridge.rb
66
+ - script/build_jsx_transform.sh
67
+ - script/build_stimulus_tests.sh
69
68
  - sig/dommy/js/quickjs.rbs
70
69
  homepage: https://github.com/takahashim/dommy-js-quickjs
71
70
  licenses:
@@ -89,7 +88,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
89
88
  - !ruby/object:Gem::Version
90
89
  version: '0'
91
90
  requirements: []
92
- rubygems_version: 3.6.2
91
+ rubygems_version: 3.6.9
93
92
  specification_version: 4
94
93
  summary: QuickJS backend for running JavaScript against a Dommy DOM.
95
94
  test_files: []
@@ -1,40 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Dommy
4
- module Js
5
- # Resolves a JS constructor by interface name for reverse construction
6
- # (`new Event(...)`, `new DOMException(...)`). The window object is the
7
- # source for most constructors — it exposes them via __js_get__ — while a
8
- # few not on the window are provided directly. Engine-agnostic.
9
- class ConstructorRegistry
10
- # The window whose __js_get__ exposes Event/CustomEvent/MouseEvent/… .
11
- attr_writer :source
12
-
13
- def initialize
14
- @source = nil
15
- end
16
-
17
- # An object responding to __js_new__ for `name`, or nil if `name` isn't
18
- # constructable (the bridge then makes the JS side throw).
19
- def resolve(name)
20
- if @source.respond_to?(:__js_get__)
21
- ctor = @source.__js_get__(name)
22
- return ctor if ctor.respond_to?(:__js_new__)
23
- end
24
- extra(name)
25
- end
26
-
27
- private
28
-
29
- # Constructors the window doesn't expose.
30
- def extra(name)
31
- case name
32
- when "DOMException"
33
- return unless defined?(Dommy::DOMException)
34
-
35
- Dommy::Bridge::Constructor.new { |args| Dommy::DOMException.new(args[0], args[1] || "Error") }
36
- end
37
- end
38
- end
39
- end
40
- end
@@ -1,55 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Dommy
4
- module Js
5
- # Bridges JS-defined custom elements to Dommy's custom element pipeline.
6
- # `customElements.define(name, JSClass)` on the JS side calls in here, which
7
- # registers a Dommy::HTMLElement subclass for `name` whose lifecycle
8
- # reactions (connected/disconnected/adopted/attributeChanged) route back to
9
- # the JS instance through the bridge. The JS class's constructor itself runs
10
- # on the JS side via the construction-stack upgrade in host_runtime.js.
11
- class CustomElements
12
- attr_writer :window
13
-
14
- def initialize(bridge)
15
- @bridge = bridge
16
- @window = nil
17
- end
18
-
19
- def define(name, observed)
20
- return unless @window.respond_to?(:custom_elements)
21
-
22
- @window.custom_elements.define(name, build_class(name, observed))
23
- nil
24
- end
25
-
26
- # customElements.upgrade(root): delegate to Dommy's registry so a subtree
27
- # attached without firing reactions gets its registered elements upgraded.
28
- def upgrade(root)
29
- return unless @window.respond_to?(:custom_elements)
30
-
31
- @window.custom_elements.upgrade(root)
32
- nil
33
- end
34
-
35
- private
36
-
37
- # A Dommy custom element class that forwards each reaction to the JS
38
- # instance. `__js_custom_element_name__` marks the node so the bridge tells
39
- # the JS side to upgrade it on first crossing (see HostBridge interface info).
40
- def build_class(name, observed)
41
- bridge = @bridge
42
- Class.new(Dommy::HTMLElement) do
43
- define_singleton_method(:observed_attributes) { observed }
44
- define_method(:__js_custom_element_name__) { name }
45
- define_method(:connected_callback) { bridge.invoke_lifecycle(self, "connectedCallback", []) }
46
- define_method(:disconnected_callback) { bridge.invoke_lifecycle(self, "disconnectedCallback", []) }
47
- define_method(:adopted_callback) { bridge.invoke_lifecycle(self, "adoptedCallback", []) }
48
- define_method(:attribute_changed_callback) do |attr, old_value, new_value|
49
- bridge.invoke_lifecycle(self, "attributeChangedCallback", [attr, old_value, new_value])
50
- end
51
- end
52
- end
53
- end
54
- end
55
- end