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.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +28 -0
- data/CHANGELOG.md +84 -1
- data/README.md +31 -9
- data/Rakefile +70 -2
- data/lib/dommy/js/quickjs/backend.rb +138 -1
- data/lib/dommy/js/quickjs/capybara.rb +31 -17
- data/lib/dommy/js/quickjs/rack.rb +15 -0
- data/lib/dommy/js/quickjs/runtime.rb +450 -42
- data/lib/dommy/js/quickjs/script_cache.rb +37 -0
- data/lib/dommy/js/quickjs/source_guard.rb +304 -0
- data/lib/dommy/js/quickjs/version.rb +1 -1
- data/lib/dommy/js/quickjs/wasm_bridge.rb +15 -15
- data/lib/dommy/js/quickjs.rb +13 -5
- data/script/build_jsx_transform.sh +27 -0
- data/script/build_stimulus_tests.sh +52 -0
- metadata +13 -14
- data/lib/dommy/js/constructor_registry.rb +0 -40
- data/lib/dommy/js/custom_elements.rb +0 -55
- data/lib/dommy/js/dom_interfaces.rb +0 -139
- data/lib/dommy/js/handle_table.rb +0 -52
- data/lib/dommy/js/host_bridge.rb +0 -400
- data/lib/dommy/js/host_runtime.js +0 -922
- data/lib/dommy/js/observable_runtime.js +0 -728
|
@@ -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
|
|
@@ -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
|
|
19
|
-
#
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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 {
|
|
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?(
|
|
126
|
-
JSValue.new(value[
|
|
127
|
-
elsif value.key?(
|
|
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?(
|
|
130
|
-
value[
|
|
131
|
-
elsif value.key?(
|
|
132
|
-
value[
|
|
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
|
data/lib/dommy/js/quickjs.rb
CHANGED
|
@@ -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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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.
|
|
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:
|
|
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.
|
|
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.
|
|
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.
|
|
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
|