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,559 @@
|
|
|
1
|
+
# ブリッジ再設計案 — 実フレームワーク(Turbo 等)の軽量テストに向けて
|
|
2
|
+
|
|
3
|
+
ステータス: ドラフト / 検討用
|
|
4
|
+
対象読者: dommy / dommy-js-quickjs / dommy-rack / capybara-dommy のメンテナ
|
|
5
|
+
最終更新: 2026-05-30
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. 目的とスコープ
|
|
10
|
+
|
|
11
|
+
### 1.1 動機
|
|
12
|
+
|
|
13
|
+
ヘッドレスブラウザ(Selenium / Cuprite / Playwright)は Turbo の「消費側」テスト
|
|
14
|
+
(レスポンスを受け取ってライブにページを書き換える挙動)には確実だが、**重い**:
|
|
15
|
+
ブラウザプロセス・実ネットワーク・レイアウト/レンダリング・V8 を伴う。
|
|
16
|
+
|
|
17
|
+
そこで **dommy(Ruby 製 DOM)+ quickjs(軽量 JS VM)+ dommy-rack(in-process Rack 接続)**
|
|
18
|
+
を土台に、**ブラウザを立てずに in-process で実フロントエンド JS(当面の標的は Turbo)を走らせて
|
|
19
|
+
テストする**、という構想がある。本書はそれを成立させるために必要な **JS⇄Ruby ブリッジの再設計**
|
|
20
|
+
を文書化する。
|
|
21
|
+
|
|
22
|
+
### 1.2 軽量 in-process がブラウザに勝てる点
|
|
23
|
+
|
|
24
|
+
「ブラウザの劣化版」ではなく、別カテゴリの利点を持つ:
|
|
25
|
+
|
|
26
|
+
- **fetch が in-process**: dommy-rack は Rack アプリを同一プロセスで呼ぶ。Turbo の `fetch` を
|
|
27
|
+
ネットワークも別サーバも無しでアプリへ直結できる。
|
|
28
|
+
- **決定論スケジューラ**: 実時計が無い = タイミングレースが消える。非同期処理は
|
|
29
|
+
「アイドルになるまでポンプ」で駆動する。
|
|
30
|
+
- **軽い起動**: quickjs の VM 起動はブラウザ/V8 比で桁違いに軽い。
|
|
31
|
+
|
|
32
|
+
この固有価値があるため、ブリッジ作り込みはオーバー投資ではなく合理的な正攻法と位置づける。
|
|
33
|
+
|
|
34
|
+
### 1.3 非ゴール
|
|
35
|
+
|
|
36
|
+
- 実ブラウザの完全な代替(レイアウト/可視性/描画依存の挙動の忠実再現)。
|
|
37
|
+
- 任意のフロントエンドフレームワークの汎用サポート。**標的は Turbo(と前提となる Stimulus)に限定**し、
|
|
38
|
+
有限の標的として駆動する。
|
|
39
|
+
- happy-dom 本家テストスイートの実行(JS 実装を直接 import する単体テストで、Ruby 実装には適用不能)。
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 実装状況サマリ(2026-05-30 時点)
|
|
44
|
+
|
|
45
|
+
この設計に沿って **軸1(型システム)と軸2(セマンティクス)はほぼ実装完了**し、
|
|
46
|
+
**実 `@hotwired/turbo` バンドルが dommy + quickjs 上で動作する**ところまで到達済み。
|
|
47
|
+
|
|
48
|
+
**完了**
|
|
49
|
+
- **軸1**: 1a インターフェースメタデータ公開 / 1b prototype 階層+`instanceof`+`Symbol.toStringTag` /
|
|
50
|
+
1c JS からの `new`(Event/CustomEvent/DOMException…)/ **1d JS 定義カスタム要素**
|
|
51
|
+
(construction stack による upgrade、connected/attributeChanged 等の lifecycle)。
|
|
52
|
+
- **軸2**: 2a ライブ/反復可能コレクション(array-like+entries-iterable)/ 2b サブオブジェクト同一性 /
|
|
53
|
+
2c メソッド同一性(per-proxy メモ化)/ 2d describe 統合・キャッシュ / 2f expando(identity 保持)+
|
|
54
|
+
prototype setter 優先(Lit 形式)。
|
|
55
|
+
- **実証**: 実 Turbo バンドルが import 時初期化を完了し、`Turbo.renderStreamMessage`
|
|
56
|
+
(turbo-stream append/prepend/update)と **turbo-frame 遅延ロード**(fetch→DOMParser→Range swap)が動作。
|
|
57
|
+
- **診断・治具**: `Runtime#on_unhandled_rejection`(握り潰し rejection を backtrace 付きで surface)/
|
|
58
|
+
`on_log` / `install_browser_globals`(bare グローバル配線)/ `BrowserHarness`(env+fetch スタブ+
|
|
59
|
+
pump+エラー/console 捕捉)。
|
|
60
|
+
- **WPT-JS 適合性固定**: 本物の `testharness.js` を載せた `WptHarness`+`WptRunner`
|
|
61
|
+
(`// META: script=` / `fetch("resources/…")` / `.html` のインライン `<script>` を解決)+
|
|
62
|
+
`rake wpt:conformance`。url/dom コーパスで適合率をスナップショット化(詳細・ROI backlog は
|
|
63
|
+
`wpt-conformance.md`)。
|
|
64
|
+
|
|
65
|
+
**この過程で見つけ・修正した実バグ**
|
|
66
|
+
- construction stack の限定(非要素 `new` がスタック上の要素を奪う問題、`chain.includes("Node")` で限定)。
|
|
67
|
+
- dehydrate/rehydrate のコールバック非対称(オブジェクト内ネスト関数が往復で壊れる)。
|
|
68
|
+
- method-only ホストオブジェクトが橋渡しされない(`wrap` を `__js_call__`/`__js_new__` も対象に)。
|
|
69
|
+
|
|
70
|
+
**残課題**
|
|
71
|
+
- Turbo Drive 全体(リンク/フォーム→ナビゲーション)、Lit/Solid。
|
|
72
|
+
- dommy 側 `__js_method_names__` の網羅(現状は Turbo 経路で必要な分のみ)。
|
|
73
|
+
- `callbacks` Map の無制限増加(長寿命 VM)、`fetch` のスタブ依存、expando の Element 限定。
|
|
74
|
+
|
|
75
|
+
> 注: `install_browser_globals` / `BrowserHarness` 等の「軽量ブラウザ環境」は、当初 Stage 1/2 として
|
|
76
|
+
> 構想したものが Runtime API +テスト治具として具体化した形。capybara-dommy ドライバ統合(Stage 2)への
|
|
77
|
+
> 接続は今後。
|
|
78
|
+
|
|
79
|
+
## 2. 現状アーキテクチャと限界
|
|
80
|
+
|
|
81
|
+
### 2.1 現状
|
|
82
|
+
|
|
83
|
+
- Ruby DOM ノードは JS 側に **無名 ES Proxy(`new Proxy({}, ...)`)** として渡る
|
|
84
|
+
(`lib/dommy/js/host_bridge.rb` の `HOST_RUNTIME_JS` 内 `makeProxy`)。
|
|
85
|
+
- プロパティ/メソッドアクセスは ABI へルーティング:
|
|
86
|
+
`__js_get__(name)` / `__js_set__(name, value)` / `__js_call__(method, args)`。
|
|
87
|
+
- メソッドか否かは各クラスの `__js_method_names__`(メソッド名のみ)で判定。
|
|
88
|
+
- `querySelectorAll` は **素の JS 配列**として返る(ライブ NodeList ではない)。
|
|
89
|
+
- backend は quickjs.rb の VM(`eval` / `define_function` / `call` / `drain_jobs!` / `gc!`)。
|
|
90
|
+
|
|
91
|
+
### 2.2 限界(実フレームワークを通せない理由)
|
|
92
|
+
|
|
93
|
+
ブリッジは「Ruby を真実の源にして、JS には薄い Proxy を見せる RPC」なので、**DOM の JS 型システム**
|
|
94
|
+
が存在しない:
|
|
95
|
+
|
|
96
|
+
| 実アプリ JS がやること | 素 Proxy だと |
|
|
97
|
+
|---|---|
|
|
98
|
+
| `node instanceof Element` / `event.target instanceof HTMLElement` | ✗ 型情報なし |
|
|
99
|
+
| `class X extends HTMLElement { connectedCallback(){} }` + `customElements.define` | ✗ `HTMLElement` が実コンストラクタでない |
|
|
100
|
+
| `new CustomEvent("x", {detail})` を JS 側で生成・dispatch | ✗ JS 型として存在しない |
|
|
101
|
+
| `Object.prototype.toString.call(el)` → `[object HTMLDivElement]` | ✗ `Symbol.toStringTag` 無し |
|
|
102
|
+
| `Array.prototype.slice.call(nodeList)` / prototype メソッド借用 | ✗ |
|
|
103
|
+
| ライブ `HTMLCollection`(`el.children` 等)の整合 | ✗ スナップショット配列 |
|
|
104
|
+
| `e instanceof DOMException` | ✗ JS 例外型として無い |
|
|
105
|
+
|
|
106
|
+
さらに **testharness.js(WPT)自体がこの型システムを前提**にする
|
|
107
|
+
(`assert_class_string` は `Object.prototype.toString.call`、`assert_throws_dom` は `instanceof DOMException`)。
|
|
108
|
+
|
|
109
|
+
加えて **コスト/セマンティクス軸**: プロパティアクセス毎に Ruby⇄JS ラウンドトリップ+marshalling が走り、
|
|
110
|
+
getter 副作用・同一性・ライブコレクション整合の再現が ABI 越しに重い。
|
|
111
|
+
|
|
112
|
+
### 2.3 規模の実測(dommy 本体)
|
|
113
|
+
|
|
114
|
+
- DOM インターフェース相当クラス: **168**(うち HTML 要素 **58**、Event 系 **18**)。
|
|
115
|
+
- `__js_method_names__`: 約 28 箇所 / 7 ファイル。**メソッド名のみ**で、継承チェーン・プロパティ一覧・
|
|
116
|
+
IDL 型は JS に未公開。
|
|
117
|
+
- 必要なサブシステム(fetch / MutationObserver / history / location / DOMParser / template / custom_elements)
|
|
118
|
+
は **dommy に既に実装済み**。よって本再設計の費用は「DOM 機能の再実装」ではなく
|
|
119
|
+
「**型付きブリッジ層+逆方向生成+各サブシステムの“JS から本物の型で見える”配線**」に集中する。
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## 3. 設計の二軸
|
|
124
|
+
|
|
125
|
+
再設計は独立だが補完的な 2 軸からなる。
|
|
126
|
+
|
|
127
|
+
### 軸 1: 型システム層
|
|
128
|
+
|
|
129
|
+
JS 側に本物のプロトタイプ階層・コンストラクタ・型タグを構築し、各ノードをその型のインスタンスとして見せる。
|
|
130
|
+
|
|
131
|
+
#### 1a. インターフェースメタデータの公開(dommy 側)
|
|
132
|
+
|
|
133
|
+
各インターフェースについて以下を返す ABI を追加(例: `__js_interface_info__`):
|
|
134
|
+
|
|
135
|
+
- インターフェース名(例 `"HTMLDivElement"`)
|
|
136
|
+
- 親インターフェース(継承チェーン: `HTMLDivElement → HTMLElement → Element → Node → EventTarget`)
|
|
137
|
+
- メソッド名一覧 / **プロパティ名一覧** / (可能なら readonly/writable)
|
|
138
|
+
|
|
139
|
+
継承は Ruby の `ancestors` から概ね導出可。**プロパティ列挙が現状ゼロ**(`__js_get__` が何でも受ける
|
|
140
|
+
開放設計)なのが主コスト。既知プロパティはプロトタイプ getter 化し、未知は Proxy フォールバックで補う方針。
|
|
141
|
+
|
|
142
|
+
> 規模: M〜L / 1〜2 週。168 クラスに触れるが自動導出+フォールバックで圧縮可能。
|
|
143
|
+
|
|
144
|
+
#### 1b. JS 側プロトタイプ階層+コンストラクタ構築
|
|
145
|
+
|
|
146
|
+
メタデータから一度だけブートストラップ:
|
|
147
|
+
|
|
148
|
+
- `EventTarget.prototype → Node.prototype → … → HTMLElement.prototype → HTMLDivElement.prototype` を
|
|
149
|
+
ABI 委譲の getter/メソッドで生成。
|
|
150
|
+
- 各 prototype に `Symbol.toStringTag` を付与。
|
|
151
|
+
- コンストラクタ関数(`window.HTMLElement` 等)を生成し `.prototype` を相互リンク。
|
|
152
|
+
- `makeProxy(handle)` がハンドル→インターフェース名を引いて正しい prototype を装着。
|
|
153
|
+
|
|
154
|
+
これで `instanceof` が自動成立。重要な ~30〜40 型に絞れる(168 全部は不要)。
|
|
155
|
+
|
|
156
|
+
> 規模: M / 1〜1.5 週。メタデータが揃えば機械的。
|
|
157
|
+
|
|
158
|
+
#### 1c. JS からの `new`(Event / CustomEvent / DOMException 等)
|
|
159
|
+
|
|
160
|
+
`new CustomEvent(...)` を **JS→Ruby `.new`→proxy** で実体化する逆方向生成。現ブリッジに無い経路。
|
|
161
|
+
小ヘルパ(例 `__rbHost.construct(interfaceName, args)`)+ Ruby 側ファクトリで対応。値型中心なら素直。
|
|
162
|
+
|
|
163
|
+
> 規模: M / 1 週。
|
|
164
|
+
|
|
165
|
+
#### 1d. `class X extends HTMLElement` + JS 定義カスタム要素 ⚠️ 最難所
|
|
166
|
+
|
|
167
|
+
Turbo の `turbo-frame` / `turbo-stream` の前提。要求:
|
|
168
|
+
|
|
169
|
+
- `HTMLElement`(および要素サブクラス)が **JS から `super()` 可能な実コンストラクタ**で、
|
|
170
|
+
構築結果が Ruby ノードに裏打ちされる。
|
|
171
|
+
- `customElements.define(name, JSClass)` に **JS クラス**を登録でき、dommy のパーサ /
|
|
172
|
+
`createElement` / upgrade ライフサイクルが **JS 側コンストラクタと `connectedCallback` /
|
|
173
|
+
`disconnectedCallback` / `attributeChangedCallback` を呼ぶ**よう双方向化。
|
|
174
|
+
- 仕様の「HTMLElement コンストラクタが upgrade 対象の既存要素を返す」トリックの実装
|
|
175
|
+
(ネイティブエンジンでも難所)。
|
|
176
|
+
- DOM 移動時の connected/disconnected の発火タイミング、`observedAttributes` の配線。
|
|
177
|
+
|
|
178
|
+
現状 dommy のカスタム要素は **Ruby クラス**(`define(name, klass)`)。JS 定義クラスが dommy の
|
|
179
|
+
upgrade ライフサイクルに参加できるよう、Ruby 側(`custom_elements` / `node_wrapper_cache`)が
|
|
180
|
+
**JS へコールバック**する必要がある。
|
|
181
|
+
|
|
182
|
+
> 規模: L / 高リスク / 2〜4 週(分散大)。**本再設計の不確実性の支配項**。
|
|
183
|
+
|
|
184
|
+
#### 1d 実装パス(確実化のための段階設計)
|
|
185
|
+
|
|
186
|
+
1d の難所は「新規発明」ではなく、**custom-elements polyfill / jsdom が実装済みの “construction stack”
|
|
187
|
+
パターン**に縮約できる。dommy は DOM 側機構(registry / upgrade / reaction / observedAttributes /
|
|
188
|
+
lifecycle)を **Ruby クラス向けに実装済み・WPT 固定済み**なので、1d は「実証済みパターンの移植+配線」
|
|
189
|
+
として確実化できる。鍵は **不確実性(分散)を Step 0 で隔離して畳む**こと。
|
|
190
|
+
|
|
191
|
+
##### 核心: construction stack を純 JS で組む(quickjs で成立する)
|
|
192
|
+
|
|
193
|
+
仕様の「コンストラクタが upgrade 対象の既存要素を返す」挙動は、**ベースクラスがオブジェクトを return
|
|
194
|
+
すると派生の `this` になる**という ES2015 規則で実現できる。quickjs は `class` / `super` / `new.target` /
|
|
195
|
+
`Reflect.construct` / `setPrototypeOf` を持つため、そのまま動く:
|
|
196
|
+
|
|
197
|
+
```js
|
|
198
|
+
const constructionStack = []; // upgrade 中の既存要素を積む
|
|
199
|
+
|
|
200
|
+
function HTMLElement() {
|
|
201
|
+
let el;
|
|
202
|
+
if (constructionStack.length > 0) {
|
|
203
|
+
el = constructionStack[constructionStack.length - 1]; // upgrade: 既存ノードを採用
|
|
204
|
+
if (el === PENDING) throw new TypeError("Illegal constructor");
|
|
205
|
+
} else {
|
|
206
|
+
const handle = __rb_create_element_for(new.target); // new MyEl(): Ruby が新ノード生成
|
|
207
|
+
el = makeProxy(handle);
|
|
208
|
+
}
|
|
209
|
+
Object.setPrototypeOf(el, new.target.prototype); // instanceof MyEl を成立させる
|
|
210
|
+
return el; // ← 派生の super() で this になる
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Ruby から呼ぶ upgrade ヘルパ
|
|
214
|
+
__rbHost.upgrade = (handle, name) => {
|
|
215
|
+
const Ctor = registry.get(name);
|
|
216
|
+
const el = makeProxy(handle);
|
|
217
|
+
constructionStack.push(el);
|
|
218
|
+
try { Reflect.construct(Ctor, [], Ctor); } // MyEl 本体実行・super() が el を採用
|
|
219
|
+
finally { constructionStack.pop(); }
|
|
220
|
+
return el;
|
|
221
|
+
};
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
この十数行が quickjs で期待通り動くか **だけ**が、1d の本質的不確実性のほぼ全て。
|
|
225
|
+
|
|
226
|
+
##### 1a/1b との合流で expando 問題が消える
|
|
227
|
+
|
|
228
|
+
ユーザのクラスは `this.count = 0` のような **DOM 外プロパティ(expando)** を書く。現状の
|
|
229
|
+
「`{}` への catch-all Proxy」はこれを `__js_set__` に流してしまい相性が悪い。これを避ける鍵:
|
|
230
|
+
|
|
231
|
+
> **1a/1b を「catch-all Proxy」ではなく「実オブジェクト+ prototype に DOM アクセサ(ABI 委譲
|
|
232
|
+
> getter/setter)」モデルで作る。**
|
|
233
|
+
|
|
234
|
+
すると expando は普通の own プロパティ、DOM アクセスは prototype 経由、`instanceof` も自然成立し、
|
|
235
|
+
`class extends HTMLElement` + `this.x=` がそのまま通る。**1d は 1a/1b の上にほぼ落ちてくる**。
|
|
236
|
+
catch-all を残したい場合は `new Proxy(realTargetWithProto, handler)` のハイブリッドも可。
|
|
237
|
+
|
|
238
|
+
##### dommy 側は「再ポイント」だけ
|
|
239
|
+
|
|
240
|
+
dommy の既存機構に足すのは 2 点のみ:
|
|
241
|
+
|
|
242
|
+
- カスタム要素定義を **「Ruby クラス or JS 定義ハンドル」** の多態にする 1 つの seam。
|
|
243
|
+
- upgrade / lifecycle が JS 定義なら `__rbHost.upgrade(handle, name)` /
|
|
244
|
+
`__rbHost.invokeLifecycle(handle, "connectedCallback", args)` を呼ぶ配線。
|
|
245
|
+
|
|
246
|
+
仕様が複雑な部分(いつ upgrade するか・reaction タイミング・observedAttributes フィルタ)は
|
|
247
|
+
**既済・WPT 固定済み**。これが「確実に」できる最大の根拠。
|
|
248
|
+
|
|
249
|
+
##### 段階パス(各段が独立に検証可・フォールバック付き)
|
|
250
|
+
|
|
251
|
+
| Step | 内容 | 検証 | 退避 |
|
|
252
|
+
|---|---|---|---|
|
|
253
|
+
| **0. 純 JS スパイク**(数時間〜1日) | 上の construction stack を **dommy 抜き**で実装。`new Derived()` と `Reflect.construct`(upgrade)、`instanceof` / prototype / コンストラクタ本体実行 / expando / Illegal guard を assert | quickjs 単体 | quirk があればここで最安で発見 |
|
|
254
|
+
| **1. 同期 createElement upgrade** | `createElement("my-el")`→ Ruby 生成 →`upgrade`→ 型付き proxy。**parser 非関与** | Ruby から「dommy ノード」かつ JS から「instanceof MyEl」 | — |
|
|
255
|
+
| **2. ライフサイクル** | connected / disconnected / attributeChanged / adopted を JS メソッドへ配線 | 各コールバック個別 | reaction を同期実行(microtask batch は後回し)= F3 |
|
|
256
|
+
| **3. parser 駆動 upgrade** | `<my-el>` のパース → 定義到着時 upgrade(`upgrade_existing`)。順序 / reaction queue の機微 | 既存 WPT カスタム要素シナリオを JS 定義で | **define-before-parse 限定** = F1 |
|
|
257
|
+
| **4. 実物** | 最小 Lit 風要素 → `turbo-frame` 本体 | Turbo シナリオ | — |
|
|
258
|
+
|
|
259
|
+
**Step 0 が最重要**: 境界(Ruby⇄JS)を入れる前に、新規カーネルを隔離して潰す。ここが通れば
|
|
260
|
+
残りは「読める配線」に収束し、不確実性(分散)が一気に畳まれる。
|
|
261
|
+
|
|
262
|
+
##### フォールバック(“確実”=段階的縮退で全否定を避ける)
|
|
263
|
+
|
|
264
|
+
- **F1: define-before-use 限定**。要素出現前に `customElements.define` 済みを要求し、最難の遡及
|
|
265
|
+
upgrade を外す。**Turbo は import 時に要素定義 → 以後に Turbo HTML 処理**なので適合。
|
|
266
|
+
テストでも define 順は制御可能。
|
|
267
|
+
- **F2: 任意 JS からの `new MyEl()` を当面外し**、createElement / parser 経路のみ。面を縮小。
|
|
268
|
+
- **F3: reaction を同期実行**(spec の microtask バッチ化は順序依存テストが要るまで保留)。
|
|
269
|
+
|
|
270
|
+
これらで動く部分集合を早期に出し、後から厳密化できる=実務的な「確実性」。
|
|
271
|
+
|
|
272
|
+
##### 既存の青写真(実証済み)
|
|
273
|
+
|
|
274
|
+
- **jsdom のカスタム要素実装**: まさに construction stack + `Reflect.construct`。裏付けノードが
|
|
275
|
+
JS か Ruby かの違いだけで、機構は直輸入可能。
|
|
276
|
+
- **@webcomponents/custom-elements polyfill**: 旧ブラウザ向けに同ダンスを純 JS で実装。
|
|
277
|
+
|
|
278
|
+
「ネイティブ実装が難所」と言いつつ **polyfill / jsdom が純 JS で解決済み**= quickjs 上でも再現できる、
|
|
279
|
+
というのが確実化の根拠。
|
|
280
|
+
|
|
281
|
+
##### 残存リスク(Step 0 で大半 retire)
|
|
282
|
+
|
|
283
|
+
1. quickjs の `Reflect.construct(C, args, newTarget)` と「ベースが return したオブジェクトが `this` に
|
|
284
|
+
なる」挙動の細部 → **Step 0 で確認**。
|
|
285
|
+
2. 同一 Ruby ノード → 同一 JS インスタンスの安定対応(handle キャッシュで概ね担保、upgrade 時に
|
|
286
|
+
紐付け固定)。
|
|
287
|
+
3. `super()` 前の field initializer 順、constructor 内 DOM 変更の reaction 再入(spec 上禁止、ガード可)。
|
|
288
|
+
|
|
289
|
+
##### 実装状況(2026-05-30)
|
|
290
|
+
|
|
291
|
+
Step 0〜3 まで実装・テスト済み(dommy 改変なし):
|
|
292
|
+
|
|
293
|
+
- **Step 0**: construction stack を quickjs で検証 → 全項目成立(instanceof / expando / backing / upgrade /
|
|
294
|
+
Illegal guard / requires-new)。`test_host_runtime.rb` に恒久ユニットテスト化。
|
|
295
|
+
- **Step 1**: `customElements.define(name, JSClass)` → JS 側 `ceRegistry` 登録 +
|
|
296
|
+
`__rb_define_custom_element` で Dommy::HTMLElement サブクラス(shim)を `window.custom_elements` に登録。
|
|
297
|
+
`createElement` 由来ノードは `__rb_host_interface` の `ce` フラグ → `makeProxy` が初回クロス時に
|
|
298
|
+
`upgradeElement`(construction stack 採用)で JS クラス化。→ `instanceof MyEl/HTMLElement`、`tagName` 成立。
|
|
299
|
+
- **Step 2**: shim の connected/disconnected/adopted/attributeChanged → `invoke_lifecycle` → JS
|
|
300
|
+
`invokeLifecycle(handle, cb)` → `this`=ノードproxy でコールバック実行。DOM 書込みが Ruby から観測可能。
|
|
301
|
+
- **Step 3**: define-after-parse の遡及 upgrade も Dommy の `upgrade_existing` 経由で動作。
|
|
302
|
+
**F1(define-before-use 限定)は不要**だった。
|
|
303
|
+
|
|
304
|
+
実装は `host_runtime.js`(construction stack / upgradeElement / invokeLifecycle / customElements 全域)と
|
|
305
|
+
新コラボレータ `CustomElements`(shim 構築・登録)に局在。
|
|
306
|
+
|
|
307
|
+
**expando / prototype アクセサ(2026-05-30 解決)**: 当初は catch-all Proxy の set が全書込みを ABI に
|
|
308
|
+
流し、`this.foo=` の expando も Lit のリアクティブ setter も失われていた。**軸2 の B で解決**(下記参照):
|
|
309
|
+
プロパティ名の列挙に頼らず、**dommy が「未知キー」を `Bridge::UNHANDLED` で通知**→JS 側が target に
|
|
310
|
+
expando として保持(**object/instance の identity も保持**)、**prototype 上の setter は set トラップより
|
|
311
|
+
優先実行**(Lit 等)。DOM プロパティ(id/className/value/textContent…)は従来どおり dommy へ。
|
|
312
|
+
|
|
313
|
+
### 軸 2: セマンティクス / コスト層
|
|
314
|
+
|
|
315
|
+
#### 2a. ライブコレクション
|
|
316
|
+
|
|
317
|
+
`el.children` / `getElementsByClassName` 等のライブ `HTMLCollection`(整数インデックストラップ+
|
|
318
|
+
`length`+iterator+`namedItem`)を Ruby 側ライブ集合へ委譲。`querySelectorAll` のスナップショット
|
|
319
|
+
(=`NodeList`)も型として整える。
|
|
320
|
+
|
|
321
|
+
> 規模: M / 1〜1.5 週。
|
|
322
|
+
|
|
323
|
+
##### 実装状況(2026-05-30・部分)
|
|
324
|
+
|
|
325
|
+
- `el.children`(`Dommy::HTMLCollection`)は dommy が `__js_get__("length")`/整数 index を公開済みで、
|
|
326
|
+
proxy 越しに `length`/`[i]`/`Array.from` が動作。**`Symbol.iterator` を array-like コレクション prototype
|
|
327
|
+
(HTMLCollection/DOMTokenList/NamedNodeMap/… )に追加**し、`for-of`/スプレッドも可に。
|
|
328
|
+
- `querySelectorAll` は `Dommy::NodeList < Array` なので wrap で**実 JS 配列**として渡り、`map`/`forEach`/
|
|
329
|
+
`for-of` がそのまま動作(追加実装不要)。
|
|
330
|
+
- **トラバーサル公開(2026-05-30 解決・dommy 改修済み)**: dommy `Element#__js_get__` に `childNodes`
|
|
331
|
+
(**live NodeList**、`@live_child_nodes` でキャッシュし `===` 安定)/ `firstChild` / `lastChild` /
|
|
332
|
+
`nextSibling` / `previousSibling` / `childElementCount` / `lastElementChild` / `nextElementSibling` /
|
|
333
|
+
`previousElementSibling` を追加。`LiveNodeList` は `NodeList` へ名前マップ+ array-like 反復可。
|
|
334
|
+
→ Solid 系の template-walk(firstChild/nextSibling/comment マーカー)の前提が揃った。
|
|
335
|
+
|
|
336
|
+
#### 2b. サブオブジェクト同一性
|
|
337
|
+
|
|
338
|
+
`el.style === el.style`、`classList`、`dataset` が毎回新 proxy だと同一性が壊れる。
|
|
339
|
+
ノード proxy ごとにサブオブジェクトをメモ化。
|
|
340
|
+
|
|
341
|
+
> 規模: M / 0.5〜1 週。
|
|
342
|
+
|
|
343
|
+
##### 実装状況(2026-05-30・解決済み)
|
|
344
|
+
|
|
345
|
+
**追加実装不要だった**。dommy が `style`/`classList`/`dataset` で**同一 Ruby オブジェクトを返す**ため、
|
|
346
|
+
HandleTable の object_id dedup + makeProxy の handle キャッシュにより `el.style === el.style` 等が既に成立。
|
|
347
|
+
テストで固定済み。
|
|
348
|
+
|
|
349
|
+
#### 2f. expando / prototype アクセサ(2026-05-30 実装)
|
|
350
|
+
|
|
351
|
+
1d の最大の制約だった「`this.foo=` の expando が永続しない / prototype setter が飲まれる」を、
|
|
352
|
+
**プロパティ名の列挙なしで**解決:
|
|
353
|
+
|
|
354
|
+
- **dommy**: `Element#__js_set__` が、未知キー(DOM プロパティでも on-handler でもないもの)に対し
|
|
355
|
+
`Bridge::UNHANDLED`(sentinel)を返す。`__rb_host_set` はこれを見て「dommy が処理したか」を真偽で JS へ返す。
|
|
356
|
+
→ **dommy のロジック自体が権威**で、別途の property-name リスト(ドリフト源)を持たずに済む。
|
|
357
|
+
- **bridge(host_runtime.js)**:
|
|
358
|
+
- get トラップ: target の **own プロパティ(=expando)を最優先**で返す(object/instance の identity 保持)。
|
|
359
|
+
- set トラップ: ① symbol→target、② **prototype 上に setter があれば `Reflect.set` で実行**(Lit 等の
|
|
360
|
+
リアクティブプロパティ)、③ それ以外は ABI へ。**dommy が未処理(false)なら target に expando 保存**。
|
|
361
|
+
- これで `el.foo = {…}`(identity 保持)、`new Ctrl()` を field に保持(メソッド維持)、Lit 形式の
|
|
362
|
+
`set label(v)` 実行、`el.id="x"`(DOM へ)が全て両立。テスト済み。
|
|
363
|
+
|
|
364
|
+
> 当初 1b で想定した「DOM プロパティを prototype アクセサ化(要・全プロパティ列挙)」までは行わず、
|
|
365
|
+
> sentinel 方式で同等の正しさを軽量に達成した。完全な prototype-アクセサ化は性能/ WebIDL 強制が要る
|
|
366
|
+
> 場面で再検討。
|
|
367
|
+
|
|
368
|
+
#### 2c. メソッド / `this` 同一性
|
|
369
|
+
|
|
370
|
+
メソッド参照のキャッシュ(フレームワークが稀に依存)。
|
|
371
|
+
|
|
372
|
+
> 規模: S〜M / 0.5 週。
|
|
373
|
+
|
|
374
|
+
##### 実装状況(2026-05-30 実装)
|
|
375
|
+
|
|
376
|
+
makeProxy のクロージャに **per-proxy のメソッドメモ化(`methodCache`)** を追加 → 同じノードを保持して
|
|
377
|
+
いる限り `el.foo === el.foo` が成立(要素を握っている前提=現実のフレームワーク用法で安定)。
|
|
378
|
+
別要素間(`a.foo === b.foo`)の同一性は保証しない(実用上ほぼ不要)。
|
|
379
|
+
|
|
380
|
+
#### 2d. 性能
|
|
381
|
+
|
|
382
|
+
プロパティアクセス毎の Ruby⇄JS ラウンドトリップ+marshalling。テスト用途なら許容圏。
|
|
383
|
+
バッチ/高速パス/メタデータキャッシュは需要次第。**初期は後回し**。
|
|
384
|
+
|
|
385
|
+
> 規模: 可変。
|
|
386
|
+
|
|
387
|
+
##### 実装状況(2026-05-30・初手)
|
|
388
|
+
|
|
389
|
+
- **proxy 生成時のホスト呼び出しを 2→1 に統合**:`__rb_host_methods` + `__rb_host_interface` を
|
|
390
|
+
廃し、`__rb_host_describe`(name / chain / methods / ce を一括返却)に。ノードがクロスする度の
|
|
391
|
+
ラウンドトリップが半減。
|
|
392
|
+
- **メソッド集合をインターフェース名でキャッシュ(`methodsByInterface`)**:同一インターフェースの
|
|
393
|
+
2 個目以降の proxy は `Set` を再構築しない。
|
|
394
|
+
- 残り(プロパティ read のバッチ化・marshalling 最適化等)は需要が出てから。
|
|
395
|
+
|
|
396
|
+
#### 2e. WebIDL 強制 / 例外セマンティクス
|
|
397
|
+
|
|
398
|
+
null/undefined・数値強制・DOMString 変換・例外を DOMException で送出。長い裾野、漸進的に。
|
|
399
|
+
|
|
400
|
+
> 規模: M(漸進)。
|
|
401
|
+
|
|
402
|
+
---
|
|
403
|
+
|
|
404
|
+
## 4. 信頼性: WPT-JS による固定と診断力
|
|
405
|
+
|
|
406
|
+
### 4.1 問題: テストの診断力が落ちる
|
|
407
|
+
|
|
408
|
+
軽量ドライバ上で Turbo を走らせると、テストが赤になったとき原因が
|
|
409
|
+
**「アプリのバグ」か「ブリッジの穴/タイミング差」か**切り分けられなくなる恐れがある。
|
|
410
|
+
テストの価値は「失敗が原因を一意に指す度合い(fault localization)」にほぼ比例するため、これは致命的。
|
|
411
|
+
|
|
412
|
+
### 4.2 解: 層ごとに独立した適合性で固定する
|
|
413
|
+
|
|
414
|
+
- **Ruby DOM 層** ← WPT(dommy 本体で既に推進中。Ruby に手移植された WPT シナリオ群)。
|
|
415
|
+
- **JS ブリッジ層** ← **WPT-JS(testharness.js を実行する WPT の JS テスト)でブリッジ自体を固定**。
|
|
416
|
+
|
|
417
|
+
WPT-JS は「適合性のおまけ」ではなく、**軽量ドライバをブラウザ代替として信頼できるものに変える中核**。
|
|
418
|
+
ブリッジが適合性で固定されて初めて、Turbo テストの赤が「アプリのバグ」を指すようになる。
|
|
419
|
+
固定が無ければ「軽いが当てにならない」になり、本来の目的を損なう。
|
|
420
|
+
|
|
421
|
+
> 軸 1(型システム)への投資は、WPT-JS の型システム検証(`instanceof` / prototype / `[object X]` /
|
|
422
|
+
> `DOMException`)を通す作業とほぼ資産共有になる。
|
|
423
|
+
|
|
424
|
+
### 4.3 被験体(subject under test)をティアで明示する
|
|
425
|
+
|
|
426
|
+
- **サーバ契約テスト(JS なし)**: 「このリクエストにこの `<turbo-stream>` / HTML を返す」。
|
|
427
|
+
capybara-dommy / dommy-rack で一意・診断的に書ける。**Turbo アプリで検証したいことの大半はここ**。
|
|
428
|
+
- **Turbo 消費テスト(JS あり・少数)**: 「frame リンクのクリックで該当 frame が差し替わる」等。
|
|
429
|
+
「JS/Turbo 統合層を触っている」と明示的にラベル付けし、ブリッジの注釈付きで受け入れる。
|
|
430
|
+
- 「ブラウザっぽい無差別スイート」(何が落ちたか言えないテスト群)は作らない。
|
|
431
|
+
|
|
432
|
+
---
|
|
433
|
+
|
|
434
|
+
## 5. 段階的実装計画
|
|
435
|
+
|
|
436
|
+
### Stage 0: 1d PoC(ゲート)
|
|
437
|
+
|
|
438
|
+
`turbo-frame` 相当の最小カスタム要素が、JS の `class extends HTMLElement` で
|
|
439
|
+
**定義 → upgrade → `connectedCallback` 発火**まで通るかを最初に検証する。
|
|
440
|
+
具体的な段階設計(**Step 0 純 JS スパイク → Step 4 実物**)とフォールバック(F1〜F3)は
|
|
441
|
+
§3「1d 実装パス(確実化のための段階設計)」を参照。**まず Step 0(dommy 抜きの純 JS スパイク)から**着手する。
|
|
442
|
+
|
|
443
|
+
- 通れば: 残りは「有限の配線作業」として見通せる。
|
|
444
|
+
- 詰まれば: コストが跳ねるので、Ruby 側 Turbo シム(§7.1)への切替を早期判断。
|
|
445
|
+
|
|
446
|
+
### Stage 1: dommy-rack + quickjs(土台)
|
|
447
|
+
|
|
448
|
+
- セッション中、持続する `window` / JS コンテキストを保持。
|
|
449
|
+
- Turbo の `fetch` → dommy-rack 経由で Rack アプリ **in-process**。
|
|
450
|
+
- `history.pushState` / `location` / DOMParser / template / MutationObserver を
|
|
451
|
+
**JS から本物の型で**配線。
|
|
452
|
+
- スケジューラのポンプ(drain microtasks → advance → idle まで)。
|
|
453
|
+
- 軸 1 の 1a–1c + **1d**、軸 2 の 2a / 2b。
|
|
454
|
+
|
|
455
|
+
### Stage 2: capybara-dommy + quickjs(ドライバ統合)
|
|
456
|
+
|
|
457
|
+
- ドライバに **「JS モード」** を追加: クリック / submit を Ruby で遷移合成せず、
|
|
458
|
+
**JS イベントとして発火**して Turbo に横取りさせる(rack-test のナビゲーション中核を JS モードで無効化)。
|
|
459
|
+
- **Capybara の暗黙待ち(`has_content?` 等の自動リトライ)↔「スケジューラをポンプして再クエリ」** に写像。
|
|
460
|
+
決定論なのでフレークしない。
|
|
461
|
+
|
|
462
|
+
> rack-test 系は本来「ナビゲーション=HTTP ラウンドトリップ / ページ作り直し / JS 状態破棄」。
|
|
463
|
+
> Turbo は逆に「クリックを JS が横取り→ fetch →同じ持続コンテキストでライブ DOM 差し替え」。
|
|
464
|
+
> Stage 2 はこのモデル衝突を「JS モード」で吸収する。
|
|
465
|
+
|
|
466
|
+
---
|
|
467
|
+
|
|
468
|
+
## 6. 非同期 / スケジューラモデル
|
|
469
|
+
|
|
470
|
+
- dommy のスケジューラは決定論(`advance_time` でのみ進む)。
|
|
471
|
+
- Turbo は `await nextRepaint()` / rAF / microtask / fetch promise / MutationObserver タイミングに依存。
|
|
472
|
+
- ハーネスは **「microtask drain → scheduler advance → 再度 drain」を quiescent になるまで反復**して駆動する。
|
|
473
|
+
- これは「Turbo が勝手に動く」のではなく **ハーネスが時計を駆動する**前提。テスト用途では制御可能で、
|
|
474
|
+
むしろ Capybara の待ちセマンティクスと自然に一致する(§5 Stage 2)。
|
|
475
|
+
- 制約: Selenium 風の `done()` コールバック型・実時間待ちはサポート外(README の既存制約と同様)。
|
|
476
|
+
|
|
477
|
+
---
|
|
478
|
+
|
|
479
|
+
## 7. 検討した代替案
|
|
480
|
+
|
|
481
|
+
### 7.1 Ruby 側 Turbo シム(実行ではなく挙動の再現)
|
|
482
|
+
|
|
483
|
+
@hotwired/turbo を実走させず、Drive / Stream の観測可能挙動を dommy-rack に Ruby で実装する。
|
|
484
|
+
|
|
485
|
+
- 長所: **1d も JS 型システムも不要**。数日〜数週。dommy の「消費者は Ruby」哲学と一致。
|
|
486
|
+
- 短所: **エミュレーション**であり実 Turbo とドリフトしうる。緑≠本番 OK、赤も「アプリ or シム未実装」
|
|
487
|
+
が切り分け困難。「Turbo 自体の正しさ」は検証できない。
|
|
488
|
+
- 使いどころ: 目的が「**Turbo アプリの挙動をテストしたい**(Turbo 自体は信頼)」なら、コスパ・哲学整合で優位。
|
|
489
|
+
|
|
490
|
+
### 7.2 DOM を JS で実装(jsdom / happy-dom 方式)
|
|
491
|
+
|
|
492
|
+
- DOM が最初から JS オブジェクトなので `instanceof` / prototype / `class extends` がタダ、ブリッジコストゼロ。
|
|
493
|
+
- だが dommy の「pure Ruby DOM」「消費者は Ruby」という価値を捨てる別製品になる。168 クラスの資産も無駄に。
|
|
494
|
+
- 「消費者が Ruby」の用途(Ruby テストが本物の DOM を直接触る)は JS 実装では埋められない。**却下**。
|
|
495
|
+
|
|
496
|
+
### 7.3 実ヘッドレスブラウザ
|
|
497
|
+
|
|
498
|
+
- Turbo 消費テストには確実だが、本構想の出発点である「**重さ**」がそのまま欠点。回避対象。
|
|
499
|
+
|
|
500
|
+
### 判断軸
|
|
501
|
+
|
|
502
|
+
| 目的 | 推奨 |
|
|
503
|
+
|---|---|
|
|
504
|
+
| アプリが正しい Turbo レスポンスを**返す**ことの検証 | dommy-rack / capybara-dommy(**JS なし**) |
|
|
505
|
+
| Turbo アプリの挙動テスト(Turbo 自体は信頼) | Ruby 側 Turbo シム(§7.1) |
|
|
506
|
+
| **実 Turbo が消費して DOM を書き換えること**の軽量検証 | 本再設計(軸 1+1d+配線、WPT-JS で固定) |
|
|
507
|
+
| フロントエンド全体の忠実検証 | 実ヘッドレスブラウザ |
|
|
508
|
+
|
|
509
|
+
---
|
|
510
|
+
|
|
511
|
+
## 8. 忠実度の天井とスコープ
|
|
512
|
+
|
|
513
|
+
軽量路線は **実ブラウザ挙動のサブセット**を追う。受け入れるギャップ:
|
|
514
|
+
|
|
515
|
+
- **得意**: frame 遅延ロード、turbo-stream 適用、form submit→stream、Stimulus、Drive の body 差し替え。
|
|
516
|
+
- **苦手 / 永続的に脆い**: 実レイアウト / 可視性依存の挙動(dommy は HTML レベル可視性のみ)、
|
|
517
|
+
DOM の稀な隅、Turbo のバージョン追従(標的が動く=適合スイートで追い続ける運用が要る)。
|
|
518
|
+
|
|
519
|
+
このギャップを **WPT-JS + Turbo シナリオの適合スイートで明示的に管理**できる限り実用になる。
|
|
520
|
+
管理しないとギャップが暗黙にテストの意味を侵食する。
|
|
521
|
+
|
|
522
|
+
---
|
|
523
|
+
|
|
524
|
+
## 9. 見積りまとめ
|
|
525
|
+
|
|
526
|
+
前提: DOM 仕様と両リポジトリに精通した開発者 1 名の集中作業。不確実性は大きめ。
|
|
527
|
+
|
|
528
|
+
| 範囲 | 内容 | 目安 |
|
|
529
|
+
|---|---|---|
|
|
530
|
+
| 型検証 + 命令的アプリ JS(1d なし) | 軸1 の 1a–1c + 軸2 の 2a/2b | **5〜7 週** |
|
|
531
|
+
| **Turbo 実走**(1d 込み) | 上記 + 1d + fetch/history/MO/template 配線 + Stage2 | **おおむね 2〜2.5 ヶ月**(1d が支配項) |
|
|
532
|
+
|
|
533
|
+
WPT-JS ハーネス整備は型検証作業と資産共有のため、上記に内包できる部分が大きい。
|
|
534
|
+
|
|
535
|
+
---
|
|
536
|
+
|
|
537
|
+
## 10. リスクと未解決点
|
|
538
|
+
|
|
539
|
+
1. **1d(JS 定義カスタム要素)の正しさ** — 最大の不確実要因。Stage 0 PoC で先に潰す。
|
|
540
|
+
2. **MutationObserver の型とタイミング** — Turbo の frame/stream 検知が依存。
|
|
541
|
+
3. **async/rAF の順序** — ポンプ設計に依存。Capybara 待ちとの写像で吸収する想定。
|
|
542
|
+
4. **fetch のブリッジ忠実度** — Response の `.text()` / headers が JS から正しく使えること(ストリーミングは不要)。
|
|
543
|
+
5. **dommy 本体への横断改修** — メタデータ公開・逆方向生成・JS 定義カスタム要素は dommy 側が
|
|
544
|
+
「JS エンジンに駆動されうる」前提のフックを持つ必要があり、**dommy ↔ dommy-js-quickjs の横断作業**になる。
|
|
545
|
+
6. **quickjs.rb の能力** — prototype/コンストラクタは `eval` で純 JS 構築でき、ネイティブクラス対応は不要。
|
|
546
|
+
Ruby から `new` を起動する小ヘルパ(`__rbHost.construct`)の追加で足りる見込み(要確認)。
|
|
547
|
+
7. **Turbo バージョン追従の運用コスト** — 標的が動く前提で適合スイートを維持する体制が要る。
|
|
548
|
+
|
|
549
|
+
---
|
|
550
|
+
|
|
551
|
+
## 11. 次アクション
|
|
552
|
+
|
|
553
|
+
1. 本書をレビューし、目的(実 Turbo 実走 / Turbo 挙動テスト)の優先度を確定する。
|
|
554
|
+
2. **§3「1d 実装パス」の Step 0(純 JS スパイク)** に着手し、construction stack が quickjs 上で
|
|
555
|
+
期待通り動く(`instanceof` / prototype / コンストラクタ本体実行 / expando / Illegal guard)ことを
|
|
556
|
+
dommy 抜きで検証する。← 最大の不確実性をここで畳む。
|
|
557
|
+
3. Step 0 が通れば Step 1(同期 createElement upgrade)へ。並行して **WPT-JS 最小ハーネス**
|
|
558
|
+
(testharness.js ロード+グローバル配線+結果回収)を立て、ブリッジ固定の足場を作る。
|
|
559
|
+
4. Step 0/1 の結果で本格実装 or §7.1 シムへの分岐を判断する。
|