個人的に使うesm.shにオフロードするbundlerを作る

こちらの作業の続き。色々思いつくことがあったのでスクラップを別にする。

プロトタイプはこれ
- esbuildのpluginを書いて
npm:<package>
をhttps://esm.sh/<package>
などに変換している - tsxのmainで書いたHTMLをhonoのjsxと見做して変換している
思ったことは以下
- 開発時にはローカルのものを参照したい -> esm.shのキャッシュさえできれば良い
- HMR的なものに対応したい -> 都度mainを変換しまくるので良いのでは?
- 全部をbundleしたくない -> それがこのコードの存在理由

上位互換として
- viteを利用
- apiを定義して利用
- jsxで書ける
が揃っている以下の記事のほうが成果物を作る分には適切そう

代替

ライブラリを作りたい
esbuildのpluginないしは何らかのコードのtranspileをするための機能をライブラリとして提供しておく必要がある。プライベートリポジトリの範囲ではdeno-bundleという名前で作っていたがもう少し適切な名前にしたい。どちらかといえばbundleというよりlinkerという感じが近い。tiny-bundleとかでも良いかと思ったがもう少しコードを減らす状況もサポートしたい。
結局のところグルーコードということなので糊(nori)とかglueとかにしてしまえば良いのでは?あるいは粘土(nendo)とかゴム(gom)とかは思いついた。特に難しく考えずにdeno-glueあたりで良い気がした。

どのような機能を保持したいか整理する。
目的というか特徴
- クライアントサイドでの試行錯誤を手軽に行いたい
- 1つのリポジトリで1つの試行ではなく複数の試行が行いたい(viteなどは1つの成果物感が強い)
- 中身が透けて見える程度に簡素なもので良い
- デプロイしたくないしバンドルもしたくない
ということを出発点としてどのような機能があれば良いかを整理してみる

欲しい機能
- クライアントサイドのコードのアプリケーション側のみをbundleしてjsに変換する
- クライアントサイドのコードでもdeno language serverで補完・型チェックが効くようにしたい
- テキトーなHTMLをjsxで組み立ててCSRがしたい
- 最終的にはesm.shに依存ライブラリの配信は任せて手軽に進捗を共有したい
- 可能なら開発環境でesm.shに読みに行くのは辞めたい(キャッシュしたい)
- 可能ならesm.shの依存を絞ったimportに変換したい(+ bundleした依存をimportしたい)

deno-glue
とりあえずプライベートリポジトリの名前を変えて色々いじって公開するか。
そして冒頭で例としてあげたプロトタイプをexampleとして載せることにする。
その後ローカルの開発環境でのキャッシュについて考える。
その後viteとの整合性やHMRについて考えてみるなどする。

deno-glue, serveコマンドを追加
↑で試してみたcacheの機能を組み込んだserveコマンドを追加することにしてみる。

ファイル監視とブラウザリロード
HMRは一旦置いておいて、ファイルを監視してのブラウザリロードを試してみる。
(これも組み込むかどうかは未定)

mainのコマンドを作る
deno run jsr:@podhmo/glue@0.2.0 init
とかできるようになった

存在しているのに見つからないとか出てだるい。
$ deno run -A jsr:@podhmo/glue@0.2.0 init error: Uncaught (in promise) NotFound: No such file or directory (os error 2): copy 'https://jsr.io/@podhmo/glue/0.2.0/examples/serve-command-example/client.tsx' -> './client.tsx'
await Deno.copyFile(path, options.to);
^
at async Object.copyFile (ext:deno_fs/30_fs.js:144:3)
at async copyFile (https://jsr.io/@podhmo/glue/0.2.0/init.ts:57:3)
at async main (https://jsr.io/@podhmo/glue/0.2.0/init.ts:31:7)
at async main (https://jsr.io/@podhmo/glue/0.2.0/main.ts:46:7)
at async https://jsr.io/@podhmo/glue/0.2.0/main.ts:56:3

ちなみにcurlでは取れる不思議
$ curl https://jsr.io/@podhmo/glue/0.2.0/examples/serve-command-example/client.tsx

fetchで迂回するコードを追加した(雑). 0.2.1で治る。

手軽に動かせるようにはなった。チュートリアルっぽい。

- APIを作った瞬間にサーバーが必要になる(デプロイが必要になる)
- デプロイ先は何が良いのだろう?
- routerのようななにかも用意しておきたいかもしれない?

react-routerを触ってみた。なんか厳しい

依存関係を真面目に追跡してdepsとして渡す
esm.shにリクエストするときに foo?deps=bar@xxx,boo@yyy
みたいな感じで依存関係を渡せるこれを明示的に指定できるようにする。直接管理するのは面倒なのでdeno.lockから計算する。

deno.json で
deno.json .imports :: alias -> pkg-path-alias
deno.lock .specifiers :: pkg-path-alias -> version
deno.lock.jsr :: @<pkg-path>@version -> {integrity, dependencies}
それ以外に直指定のものが存在する e.g. npm:react@18

specifierをaliasに書き換えてreplaceしないとだめなのか。

"npm:react-dom@18": "18.3.1_react@18.3.1",
こういう設定があるみたい?
これは import { createRoot } from "https://esm.sh/react-dom@18.3.1/client?deps=react@18.3.1";
とかに変換してあげないといけない。

更にこういうのもあるらしい
"npm:react-router@7": "7.1.1_react@18.3.1_react-dom@18.3.1__react@18.3.1",

ようやく依存関係の本体に勸められそうだ。
deno.lockを覗いてみると、jsrに関してはspecifiersのところで記述されている形式で依存が記述されている
"specifiers": {
"jsr:@hono/hono@4.6.15": "4.6.15",
"jsr:@podhmo/with-help@0.5": "0.5.3",
"jsr:@std/assert@*": "1.0.6",
"jsr:@std/assert@1.0.6": "1.0.6",
"jsr:@std/cli@1": "1.0.9",
"jsr:@std/collections@1.0.9": "1.0.9",
"jsr:@std/fs@1.0.4": "1.0.4",
"jsr:@std/fs@^1.0.5": "1.0.8",
"jsr:@std/internal@^1.0.4": "1.0.5",
"jsr:@std/json@1": "1.0.1",
"jsr:@std/jsonc@^1.0.1": "1.0.1",
"jsr:@std/path@1.0.6": "1.0.6",
"jsr:@std/path@^1.0.6": "1.0.8",
"jsr:@std/path@^1.0.8": "1.0.8",
"npm:esbuild-wasm@0.24.2": "0.24.2",
"npm:preact@*": "10.5.13",
"npm:preact@10.5.13": "10.5.13"
},
"@std/json@1.0.1": {
"integrity": "1f0f70737e8827f9acca086282e903677bc1bb0c8ffcd1f21bca60039563049f"
},
"@std/jsonc@1.0.1": {
"integrity": "6b36956e2a7cbb08ca5ad7fbec72e661e6217c202f348496ea88747636710dda",
"dependencies": [
"jsr:@std/json"
]
},

npmの方は specifierとは限らない場合がありそう?
"specifiers": {
"jsr:@hono/hono@4.6.15": "4.6.15",
"jsr:@podhmo/glue@0.2.1": "0.2.1",
"jsr:@std/json@1": "1.0.1",
"jsr:@std/jsonc@^1.0.1": "1.0.1",
"npm:@types/react@18": "18.3.18",
"npm:@types/react@19": "19.0.2",
"npm:esbuild-wasm@0.24.2": "0.24.2",
"npm:react-dom@18": "18.3.1_react@18.3.1",
"npm:react-dom@19": "19.0.0_react@19.0.0",
"npm:react-router@7": "7.1.1_react@18.3.1_react-dom@18.3.1__react@18.3.1",
"npm:react@18": "18.3.1",
"npm:react@19": "19.0.0"
},
"react-router@7.1.1_react@18.3.1_react-dom@18.3.1__react@18.3.1": {
"integrity": "sha512-39sXJkftkKWRZ2oJtHhCxmoCrBCULr/HAH4IT5DHlgu/Q0FCPV0S4Lx+abjDTx/74xoZzNYDYbOZWlJjruyuDQ==",
"dependencies": [
"@types/cookie",
"cookie",
"react@18.3.1",
"react-dom@18.3.1_react@18.3.1",
"set-cookie-parser",
"turbo-stream"
]
},
"react-dom@18.3.1_react@18.3.1": {
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"dependencies": [
"loose-envify",
"react@18.3.1",
"scheduler@0.23.2"
]
},
"set-cookie-parser@2.7.1": {
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="
},