ベランダピクニックと Parse HTML on bun

昨日は娘から寝かしつけられてしまったので昨日の日記を残す。

お昼ご飯はパンにしようということで家族でパン屋へ。最近娘がはまっている目当てのメープルラウンドはなかったが、サンドイッチなどを買い込んで帰宅。天気が良かったし日陰ならちょっと暑いが過ごせるくらいの気温だったので、ベランダピクニックをすることに。余談だが今の家はベランダで小さめのテントなら余裕で立てることができるくらいには広いので気に入っている。

我が家のベランダピクニックは、ピクニックシートを敷いてアウトドアベンチを置く簡単なもの。ほどよく風が吹いていて気持ち良かった。

食事後は娘と軽く読書。娘は最近自分の名前を書きたいようで、お絵かきボードを持ち出しては文字らしき短めの線をいくつも書いて、線をなぞりながら自分の名前を復唱している。

娘が書いた署名


午後からはゆっくり。娘が昼寝に入ったので、最近課題となっていた「インプットしたURLを集積する機構」に取りかかることに。すでに ChatGPT とやりとりはしていて設計概要はできているもの。

  1. 自作の iOS ショートカットにより、iOS アプリからインプットURLを取得して GitHub Actions Workflow にリクエスト

  2. 起動された GitHub Actions で Node.js スクリプトを実行し、入力されたURLからページタイトルや Twitter コンテンツを取得して Markdown 形式で標準出力

  3. GitHub Actions で集積ファイルのファイル変更・コミットする

色々プロトタイピングした結果、これで良いのではないかという結果に至っている。


iOS ショートカットは、iOS メモアプリに出力するまではできている (ここで Markdown までは出力できているが、細かい制御がしづらい) ので、URL を POST するのは比較的簡単にできる。GitHub Actions on: repository_dispatch の起動には PAT が必要なので管理だけちゃんとする必要がある。


このリクエストを受けて動き出すワークフローはこちら


様々な観点で便利な Bun を利用しようかと思って、Markdown コンテンツの取得は `bun scripts/add-input.ts $url` としているが、ここで少し問題がある。まだ未解決。


URLから #fetch でページを取得するわけだが、HTMLパースをどうするかという問題だ。現在の要件だと head.title か head.meta だけ取得できたら十分なので、ヘッドレスブラウザはオーバーキルでありできれば避けたい。

ということで happy-dom を使ってみたのだが、そこで問題が生じた。(jsdom は重いということなのでまだ試していない。ヘッドレスブラウザよりは早そうなので選択肢としては残っている)

❯ bun scripts/add-input.ts https://bun.sh/docs/runtime/bunfig
42 |         throw new Error("Failed to retrieve the content. No title found");
43 |       }
44 |       return title.textContent || "";
45 |     }
46 |   } catch (error) {
47 |     throw new Error(
              ^
error: An error occurred while fetching the content: this._getWindow is not a function. (In 'this._getWindow()', 'this._getWindow' is undefined)
      at /Users/yoshikouki/src/github.com/yoshikouki/yoshikouki/scripts/add-input.ts:47:10
70 |      * @see https://www.quirksmode.org/js/events_order.html#link4
71 |      * @param event Event.
72 |      * @returns The return value is false if event is cancelable and at least one of the event handlers which handled this event called Event.preventDefault().
73 |      */
74 |     dispatchEvent(event) {
75 |         const window = this._getWindow();
                           ^
TypeError: this._getWindow is not a function. (In 'this._getWindow()', 'this._getWindow' is undefined)
      at dispatchEvent (/Users/yoshikouki/src/github.com/yoshikouki/yoshikouki/node_modules/happy-dom/lib/event/EventTarget.js:75:23)
      at dispatchEvent (/Users/yoshikouki/src/github.com/yoshikouki/yoshikouki/node_modules/happy-dom/lib/event/EventTarget.js:97:20)
      at /Users/yoshikouki/src/github.com/yoshikouki/yoshikouki/node_modules/happy-dom/lib/nodes/document/Document.js:862:12

このような TypeError が表示されるのだ。ちなみに https://example.com ならエラーは出るものの出力まで至る (出力1行目の `[Example Domain](https://example.com)` が期待するもの)

❯ bun scripts/add-input.ts https://example.com
[Example Domain](https://example.com)
70 |      * @see https://www.quirksmode.org/js/events_order.html#link4
71 |      * @param event Event.
72 |      * @returns The return value is false if event is cancelable and at least one of the event handlers which handled this event called Event.preventDefault().
73 |      */
74 |     dispatchEvent(event) {
75 |         const window = this._getWindow();
                           ^
TypeError: this._getWindow is not a function. (In 'this._getWindow()', 'this._getWindow' is undefined)
      at dispatchEvent (/Users/yoshikouki/src/github.com/yoshikouki/yoshikouki/node_modules/happy-dom/lib/event/EventTarget.js:75:23)
      at dispatchEvent (/Users/yoshikouki/src/github.com/yoshikouki/yoshikouki/node_modules/happy-dom/lib/event/EventTarget.js:97:20)
      at /Users/yoshikouki/src/github.com/yoshikouki/yoshikouki/node_modules/happy-dom/lib/nodes/document/Document.js:862:12

さらに、 happy-dom の Window ではなく GlobalWindow を使うと、https://example.com なら正常に動く。他のドメインでは出力を得つつもエラーになる

❯ bun scripts/add-input.ts https://example.com
[Example Domain](https://example.com)
❯ bun scripts/add-input.ts https://bun.sh/docs/runtime/bunfig
[bunfig.toml – Runtime | Bun Docs](https://bun.sh/docs/runtime/bunfig)
259 |             });
260 |             return;
261 |         }
262 |         // For BR
263 |         if (contentEncodingHeader === 'br') {
264 |             body = Stream.pipeline(body, Zlib.createBrotliDecompress(), (error) => {
                                             ^
TypeError: Zlib.createBrotliDecompress is not a function. (In 'Zlib.createBrotliDecompress()', 'Zlib.createBrotliDecompress' is undefined)
      at onResponse (/Users/yoshikouki/src/github.com/yoshikouki/yoshikouki/node_modules/happy-dom/lib/fetch/Fetch.js:264:41)
      at node:http:862:28
      at processTicksAndRejections (:55:76)


すでに Issue 化は済まされているので、時間はかかるが修正されるかもしれない


このエラーの原因は追っていないが、こういうところで原因を追及するとよいインプットになりそうだなーと思いつつも行動には移していない。こういうところだよなあ


さて、ではこのシステムではどうするのかという選択肢は以下になるか。徐々に対応するレイヤーを変えていく感じ。

  1. happy-dom 以外のブライザレスライブラリを使う

    1. https://github.com/jsdom/jsdom

    2. https://github.com/cheeriojs/cheerio

  2. ヘッドレスブラウザを使う

  3. Bun ではなく deno か node を使う

一応 happy-dom ではなく、 parse5 を使って DOM 化していない HTML 解析結果を直接見るというのも手だが、それだと難易度や工数が上がるのと、スケール性に難があるので避けたいところ。リクルートのエンジニアコース新卒研修のブラウザを読んでいたので、ここら辺の理解をスッとできたのは良かったな


Bun が 1.0 になって安定版になったとはいえ、まだ新しいエコシステムなのでこういうバグを踏み抜く確率は高いよなあ、などと当たり前のことを思った


いいなと思ったら応援しよう!