Claude Opus4.6を使用し、自作ライブラリの試作品を公開しました。
リリースしてから1週間が経つので、改めて今回は、自身がなぜこのライブラリを作るに至ったか、その経緯と解決したかった課題について書きます。
そもそもの課題までの経緯
普段の私は業務でSpring MVCを使用した業務アプリを作成しています。Thymeleafを使用したサーバーサイドレンダリングがメインなので、JavaScriptはフロントを少しだけ賑やかにするパーツという扱いでした(jQueryメイン)。私自身は型定義が曖昧で、ライブラリ依存になりがちな記述というものが好きではないため、仕事では極力vanillaなJSで書くようにし、そこにJSDocを書きまくってなるべく型依存な書き方をする、そして家ではd.tsを定義しTypeScriptを使用する。そんな毎日を過ごしています。
あるとき、Spring MVCのプロジェクトにも関わらずサーバーサイドに肉薄するほどフロントの実装が大掛かりな案件に参画しました。そこでJSの勉強をしながら実装していたのですが、勉強する中でWeb ComponentというAPIがあることを知りました。
Web ComponentのAPIを読んだ瞬間、自分の中で要素の扱い方にゴールが出来上がりました。Web ComponentのshadowRootをclosedで設定することを全体で共有していたら、要素同士の結合度はどうやっても下がることになると思ったのです。実際に業務でも、仮にA、B、Cという要素が存在し、それぞれの依存関係がA→B→Cの順にある場合、
- Aのイベント発火→Bを動的に変更→Cを動的に変更
- Bのイベント発火→Cを動的に変更
といった形で、誰かが変化した時に自身の変更に影響される要素を変化元が直接コントロールするという状況が発生していました。そしてこの実装は、A、B、Cとは別にCの依存元としてDという新たな要素が追加されると管理がキツくなります。というのも
- Aのイベント発火→Bを動的に変更+Cを動的に変更
- Aのイベント発火→Dを動的に変更+Cを動的に変更
のように複数の要素が一つの要素に変更を加えるようなことが発生した場合に、B以降の同期を取ることが難しくなってしまうためです。なぜならBとDはお互いにどちらが先に変更したのかなどを管理することができないため、どうやってもCが自身の状況を管理することができません。
このことを考えると、というかこういう問題が発生しないようにするためにも最初からA→Bのように変化元が直接変化先を変えるのではなく、Bが自身の変更に影響するAを監視する方向に持っていく方が結果的にバグの生まれないソースになります(Proxyによる実装だとプロパティの変更検知はできても、複数の依存元からの変更の順序制御や依存グラフ全体の管理には対応できないため、結局はObserverパターンの実装、有向非巡回グラフ(DAG)などを利用したAの変化をBとDがそれぞれ監視するという方向性にするしかない)。
上記の悩みもあったため、レイアウトの構成の完全な分離、他の要素からの直接アクセスを禁止にする仕様、Proxy的なプロパティの監視、といった強制的に疎結合にすることができるこの機能はとても魅力的でした。
Web Componentの実装の煩雑さ
しかしその実装は正直かなりめんどくさいです。HTMLElementの拡張をして、コンストラクター関数の定義、カスタム要素に必要なメソッドの定義等、手軽さとはかけ離れています。もっと一つのカスタム要素という感覚で記述でき、すべての実装が綺麗にカプセル化されるような仕様が欲しいと思うようになりました。
普段自宅ではSvelteを使用して遊んでいたので、実際にSvelteのカスタム要素定義による疎結合を考えました。しかしSvelteでカスタム要素を定義すると、ShadowRootはopenにすることしかできず、しかも実際にそのカスタム要素を使用するには、使用する側がSvelte Componentをimportした上で、そのComponentを使用する際はimportしたときのファイル名ではなく、スパイナルケースで記述する必要がありました(ファイル名通りのキャメルケースを記述するとWeb Componentとして解釈されない)。結局自分の理想とは程遠く、他のライブラリを検索してもclosedなShadowRootで管理できて、かつSvelteのようなコンポーネント用のファイルひとつで簡単に作れるライブラリは存在しないことを知りました。
自作を試みた結果ぶつかった壁
そこで私は、HTMLの中にカスタム要素となるタグ、その中に要素+Styleタグ+Scriptタグをセットで記載し、画面上に描画されているカスタム要素らしき名前のものをカスタム要素に変換することができるようにしました。画面上にあるDOMのうち、最も根っこにある子要素から指定の名前を持つものを再帰的に検知し、そのカスタム名の配下にある要素をすべて、動的に生成したカスタム要素が持つclosedなShadowRootに突っ込むことで、要素側がWeb Componentとして定義されることを意識しなくても動くように設計したのです。このことでコンポーネント間の結合度はグッと低くなり、イベントによる連携を余儀なくされるようになります。しかしこれにはどうしようもない課題が2点ほどありました。
課題1: Scriptタグの実行タイミング
動的にカスタム要素を定義する際、カスタム要素として定義したい要素群の中にScriptがあったとして、そのScript群がカスタム要素の動的生成より先に実行される保証がありませんでした。記述の順番を整理するといった簡易的な部分で解決するような内容ではなく、そもそもShadowRootの中に要素群を突っ込むタイミングでScriptは動かなくなります(HTML5の仕様上、innerHTMLで挿入されたscriptタグは実行されません)。
かといってScriptタグが所属するカスタム要素を動的に生成する前にScriptタグの処理が終わるかどうかなんて、確認のしようがありません(そもそもカスタム要素自体元々ないのだから)。そして仮にタイミングを完璧にコントロールできたとしても、要素群がShadowRootに突っ込まれた時点で参照が切れてしまうため、要素そのものにアクセスする処理を書くことができません(あくまでclassに所属するメソッドなら要素へのアクセスができるけどScriptタグはカスタム要素の外にあるためアクセスすることが不可能。Shadow DOM内のscriptからは自身のshadow rootにアクセスする手段がありません)。
課題2: セキュリティの犠牲
1つ目の課題を解決しようと思った場合にどうしてもセキュリティを犠牲にする必要がありました。動的に生成されるカスタム要素の中で、そのカスタム要素に所属している状態で内部管理されている要素に直接アクセスする場合、カスタム要素の動的生成時に動的な実行をする必要がありました。この場合、Scriptタグにnonce属性を指定していたとしても、動的に実行する時点で効力を失います。つまり実質的に生のScriptタグを書いているのと同じことになってしまうのです。
Chasketの誕生と現状
結局それらの課題を解決することがどうしてもできなかったため、最初からJSとして定義できるような仕組みを作ることにしました。それがChasketです。
しかしコンパイラの実装経験がなかったため、新しいファイル形式の設計までは自分で行いましたが、実際のコンパイラはClaude Opus4.6に実装してもらいました。最初こそ色々口を出していましたが、途中からついて行けなくなり、アイディアは出せても実装に関与できない状態になってしまいました。
そのため微妙に想定と違う内容(元々はclosedなShadowRootオンリーの想定だったのにopenがデフォルトになっている等)が生まれていたり、自分でソースを追うことができなかったりと、人に使ってもらいたいと思ってもなかなか難しい状況です。
これからの課題
自分でこのコンパイラの原理や発想を理解し、自分でメンテナンスできるようになる必要があると思っています。そうでないときっと使おうと思っても聞く相手もいないため、触ることをやめてしまうだろうと思います。
なんとかこのライブラリを自分が製作者だと言えるようになるためにも、ライブラリそのものの理解を深め、周りの人に使ってもらえたらなと思います。
ここまで読んでいただきありがとうございました。
よかったらライブラリのRepositoryにスターやIssueを投稿してもらえると励みになります。