client-side-js について
Val Townでブラウザー向けJavaScriptモジュールをビルド工程なしで配信するためのスキルです。開発者向けに、トランスパイル、インポート、イミュータブルキャッシュ、検証方法を説明します。
主な機能
- 01TS、TSX、JSXの配信と変換
- 02バージョン付き不変ファイルURL
- 03明示的なURLによるブラウザーインポート
- 04レスポンスとブラウザーエラーの確認
活用シーン
- 01バンドラーなしでブラウザーUIを構築
- 02Valからローカルモジュールとアセットを配信
- 03クライアントファイルを再訪時にキャッシュ
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ val-town/ plugins --skill client-side-jsインストール後、Claude Code で/client-side-jsを入力して読み込まれたか確認します。
詳細
クライアントサイド JavaScript
Val Town にはビルドステップもバンドラーもありません。クライアントサイドモジュールは、HTTP 経由で配信する Val 内のファイルにすぎません。Val Town がリクエストごとにトランスパイルします。ファイルを返すルートを <script type="module"> で指定すると、ブラウザーがそのファイルを実行します。設定は不要です(webpack/vite/esbuild も不要です)。
モジュールの配信
std/utils の serveFile はファイルを読み込み、適切な Content-Type で配信します。.ts、.tsx、.jsx ファイルはJavaScript にトランスパイルし、型を取り除いて JSX をコンパイルしてから、text/javascript として配信します。ソースファイルを配信すれば、ブラウザーには実行可能な JS が届きます。
import { serveFile } from "https://esm.town/v/std/utils/index.ts";
// in any HTTP handler — serve a client module at some URL path
app.get("/app.tsx", (c) => serveFile("/app.tsx"));
次に、HTML から読み込みます。
<script type="module" src="/app.tsx"></script>
配信するパスとファイルの場所は自由に決められます。よく使われる簡便な方法は、ワイルドカードでディレクトリ内のモジュールとアセットをまとめて配信することです。
app.get("/client/**/*", (c) => serveFile(c.req.path));
serveFile はデフォルトで現在の Val を参照します。エントリーポイントではないファイルから呼び出してパスが解決されない場合は、2 番目の引数に import.meta.url を渡してください。
デフォルト:バージョン付きで不変キャッシュされるモジュール
serveImmutableFile を使うと、ブラウザーにファイルを不変キャッシュさせて Val のフロントエンドを高速化できます。公開すると Val のバージョンが上がり、キャッシュは自動的に無効になります。計測結果:再訪時は665ms → 157ms、アセットのリクエストはゼロです。
import { immutableFileUrl, serveImmutableFile } from "https://esm.town/v/std/utils/index.ts";
app.get("/__immutable/*", (c) => serveImmutableFile(c.req.path));
キャッシュされない HTML シェルで、エントリーモジュールにバージョンを付与します。
immutableFileUrl("/frontend/index.tsx") → /__immutable/42/frontend/index.tsx
(42 は現在の Val のバージョンです)。相対インポートは同じプレフィックス内で解決されるため、バージョンの付与が必要なのはエントリーだけです。1 つのルートとバージョン付き URL 1 つで、クライアントの依存関係グラフ全体をカバーできます。
- 古いバージョンの URL は公開後に 404 になります(Next.js のビルドアセットと同様)。再読み込みすると新しいバージョンが取得されます。
- HTML シェルを変更せずに既存の Val を移行する場合は、以前のファイルルートも
serveImmutableFileに向けます。すると、通常のパスは 302 でバージョン付きの領域に転送されます。ページビューごとにリダイレクトは 1 回です。
代替方法:esm.town から直接配信する
どの Val ファイルにも、オンデマンドでトランスパイルする公開 esm.town URL がすでにあるため、serveFile を使わず、スクリプトからその URL を直接指定できます。
<script type="module" src="https://esm.town/v/youruser/yourval/app.tsx"></script>
通常は serveFile を使うのがおすすめです。管理できるパスから同一オリジンでモジュールを配信でき、自分の Val の URL をハードコードする必要もありません。
ブラウザーでのインポートの解決方法
トランスパイラーはインポートをバンドルしたり書き換えたりしません。型と JSX を取り除くだけです。そのため、クライアントモジュール内のすべてのインポートは、ブラウザーが URL として取得できるものである必要があります。
-
ローカルインポートには明示的な拡張子が必要です。
import { x } from "./util.ts"は/util.ts(または配信パスからの相対パス)に解決され、同じルートかワイルドカードで配信されている必要があります。拡張子を省略すると(./util)、404 になります。 -
サードパーティーの依存関係には、完全な ESM URL が必要です。
import React from "react"のようなベアスペシファイアはブラウザーで解決されません。バージョンを固定したうえで、esm.sh などの CDN からインポートしてください。import { createRoot } from "https://esm.sh/[email protected]/client";
クライアントコードでベアスペシファイアを使いたい場合は、HTML にインポートマップを記述する方法もあります。
同じ仕組みは、React、バニラ DOM スクリプト、canvas のゲームループ、Alpine、htmx など、あらゆるクライアントコードで使えます。異なるのはインポートだけです。依存関係のないプレーンな .ts モジュールなら、CDN から読み込むものはありません。
React 固有の注意点
React 関連のインポートはすべて同じバージョン(18.2.0)に固定し、React に依存するライブラリには [email protected],[email protected] を指定してください。コピーのバージョンが一致しないと、Cannot read properties of null (reading 'useState') が発生します。JSX とスタイルの規約については react-ui スキルを参照してください。
避けること
- インラインの
<script>ブロックやテンプレート文字列の HTML にアプリのロジックを書かないでください。 クライアントコードは実際の.ts/.tsxファイルに記述し、型チェック、Lint、レビューができるようにしてください。インラインのブートストラップを数行書くのは問題ありませんが、アプリ本体は書かないでください。 - バンドラーやビルドコマンドは使いません。 追加すべきビルドステップはありません。
- Hono の
serveStaticは Val Town では動作しません。serveFileを使用してください。
変更の検証
モジュールの URL(例:/app.tsx)を取得し、HTML やエラーではなく text/javascript が返されることを確認してください。HTML シェルに https://esm.town/v/std/catch を追加して、ブラウザーのエラーが get_logs に送られるようにし、ページを読み込んでログを確認してください。両方を確認するまでは、変更が完了したと報告しないでください。