次のような場合に使用: ブラウザで動作するJavaScriptを配信する必要がある場合。React アプリ、素のDOM スクリプト、Canvas/ゲーム、htmx/Alpine、または単一のインラインコード以上の複雑なクライアント側モジュールに対応します。Val Town がビルド工程(コンパイル処理)なしに変換済みの .ts/.tsx/.jsx モジュールをどのように配信するのか、ブラウザがそれらのインポートをどのように解決するのか、そして外部の依存パッケージをどのように読み込むのかについて説明します。
Use when a val needs to ship JavaScript that runs in the browser — React apps, vanilla DOM scripts, canvas/games, htmx/Alpine, or any client-side module beyond a single inline snippet. Explains how Val Town serves transpiled .ts/.tsx/.jsx modules with no build step, how the browser resolves their imports, and how to load third-party deps.
Val Townはビルドステップもバンドラーも不要です。クライアント側のモジュールは、HTTPで配信するval内のファイルに過ぎず、Val TownがリクエストごとにそれをJavaScriptに変換します。<script type="module">でファイルを返すルートを指定すれば、ブラウザが実行します。設定は一切不要です(webpackやvite、esbuildは使いません)。
std/utilsのserveFileはファイルを読み込み、正しいContent-Typeで配信します。.ts、.tsx、.jsxについてはJavaScriptにトランスパイル(型情報を削除、JSXをコンパイル)して、text/javascriptとして配信します。ソースファイルを配信すれば、ブラウザが実行可能なJavaScriptを受け取ります。
import { serveFile } from "https://esm.town/v/std/utils/index.ts";
// HTTPハンドラー内で、クライアントモジュールをURLパスで配信
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を公開するたびにバージョンが上がって自動的に無効化されるため、フロントエンドが高速になります。測定結果:リピート訪問時に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つのルートと1つの印字されたURLでクライアント全体をカバーします。
serveImmutableFileに向ければ、素のパスはバージョン付きアドレスに302リダイレクトされます(ページビューごとに1回のリダイレクト)。すべてのvalファイルは、オンデマンドでトランスパイルする公開esm.town URLを既に持っているため、serveFileをスキップして、スクリプトを直接そこに向けることもできます:
<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/react-dom@18.2.0/client";
HTMLのインポートマップを使えば、クライアントコードでベアなスペシファイアを使うこともできます。
同じモデルはどのクライアントコードにも使えます — React、バニラDOM、キャンバスゲームループ、Alpine、htmx。インポートの方法が異なるだけで、依存がない素の.tsモジュールならCDNから読み込むものは何もありません。
React関連のインポートはすべて同じバージョン(18.2.0)に固定し、Reactに依存するライブラリには?deps=react@18.2.0,react-dom@18.2.0を付けてください。バージョンが混在するとCannot read properties of null (reading 'useState')エラーが起きます。JSXとスタイリング規約についてはreact-uiスキルを参照してください。
<script>タグやテンプレート文字列HTMLにアプリロジックを書かない。 クライアントコードは実ファイル(.ts/.tsx)に置いて、型付け、linting(コード検査)、レビューを有効にしてください。数行のブートストラップ(初期化処理)ならインラインで構いませんが、アプリ本体は違います。serveStaticはVal Townで動作しません — 代わりにserveFileを使ってください。モジュールのURL(例:/app.tsx)を取得し、text/javascriptが返されることを確認してください(HTMLやエラーでないこと)。HTMLシェルにhttps://esm.town/v/std/catchを追加してブラウザエラーをget_logsにパイプしてから、ページを読み込んでログを確認してください。双方の確認なしに完了とは報告しないでください。
Val Town has no build step and no bundler. A client-side module is just a file
in your val that you serve over HTTP; Val Town transpiles it per request. You point
a <script type="module"> at a route that returns the file, and the browser runs
it. There is nothing to configure (no webpack/vite/esbuild).
serveFile from std/utils reads a file and serves it with the correct
Content-Type. For .ts, .tsx, and .jsx it transpiles to JavaScript —
strips types, compiles JSX — and serves text/javascript. You serve the source
file; the browser receives runnable 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"));
Then load it from your HTML:
<script type="module" src="/app.tsx"></script>
The path you serve at and the file's location are up to you. A common shortcut is a wildcard that serves a whole directory of modules and assets:
app.get("/client/**/*", (c) => serveFile(c.req.path));
serveFile defaults to the current val. If you call it from a non-entrypoint file
and paths don't resolve, pass import.meta.url as the second argument.
serveImmutableFile makes your val's frontend faster by letting browsers cache
files immutably; publishing bumps the val's version, which invalidates
automatically. Measured: repeat visits 665ms → 157ms with zero asset requests.
import { immutableFileUrl, serveImmutableFile } from "https://esm.town/v/std/utils/index.ts";
app.get("/__immutable/*", (c) => serveImmutableFile(c.req.path));
In the never-cached HTML shell, stamp the entry module:
immutableFileUrl("/frontend/index.tsx") → /__immutable/42/frontend/index.tsx
(42 = the val's current version). Relative imports resolve under the same prefix,
so only the entry needs stamping — one route and one stamped URL cover the whole
client graph.
serveImmutableFile — bare paths then 302 into versioned space,
at one redirect per page view.Every val file already has a public esm.town URL that transpiles on demand, so you
can skip serveFile and point a script straight at it:
<script type="module" src="https://esm.town/v/youruser/yourval/app.tsx"></script>
serveFile is usually preferred because the module is served same-origin from a
path you control, and you don't have to hardcode your own val URL.
The transpiler does not bundle or rewrite imports — it only strips types and JSX. So every import in a client module must be something the browser can fetch as a URL:
Local imports need explicit extensions. import { x } from "./util.ts"
resolves to /util.ts (or relative to the served path) and must be served too —
by the same route or a wildcard. Omitting the extension (./util) 404s.
Third-party deps need full ESM URLs. Bare specifiers like import React from "react" don't resolve in the browser. Import from a CDN such as esm.sh, with
versions pinned:
import { createRoot } from "https://esm.sh/react-dom@18.2.0/client";
An import map in the HTML is an option if you want bare specifiers in client code.
The same model works for any client code — React, vanilla DOM scripts, a canvas
game loop, Alpine, htmx. Only the imports differ; for a plain .ts module with no
dependencies there's nothing to load from a CDN at all.
Pin all React-family imports to the same version (18.2.0) and pass
?deps=react@18.2.0,react-dom@18.2.0 on libraries that depend on React. Mismatched
copies cause Cannot read properties of null (reading 'useState'). See the
react-ui skill for JSX and styling conventions.
<script> blobs or template-string HTML. Put client
code in real .ts/.tsx files so it's typed, linted, and reviewable. A few lines
of inline bootstrap are fine; the app is not.serveStatic from Hono does not work on Val Town — use serveFile.Fetch the module's URL (e.g. /app.tsx) and confirm it returns text/javascript,
not HTML or an error. Add https://esm.town/v/std/catch to the HTML shell to pipe
browser errors into get_logs, then load the page and check the logs. Don't report
the change as done without both.
原文・著作権は Anthropic および各プラグイン作者に帰属します。日本語訳は Claude API による自動翻訳です。