🌐client-side-js
- プラグイン
- valtown
- ソース
- GitHub で見る ↗
説明
次のような場合に使用: valがブラウザ上で動作するJavaScriptを配信する必要があるとき — Reactアプリ、バニラDOMスクリプト、canvas/ゲーム、htmx/Alpine、またはインラインスニペット一つに収まらないあらゆるクライアントサイドモジュール。 Val Townがビルドステップなしで`.ts`/`.tsx`/`.jsx`モジュールをトランスパイルして配信する仕組み、ブラウザがそれらのimportを解決する方法、およびサードパーティの依存関係を読み込む方法について説明します。
原文を表示
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.
ユースケース
- ✓ブラウザ上でJavaScriptを配信する必要があるとき
- ✓Reactアプリを実行するとき
- ✓バニラDOMスクリプトを実行するとき
- ✓canvas/ゲームを実行するとき
- ✓htmx/Alpineを実行するとき
本文(日本語訳)
クライアントサイド JavaScript
Val Town にはビルドステップもバンドラーも存在しません。クライアントサイドモジュールは、val 内のファイルを HTTP 経由で配信するだけです。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";
// 任意の 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 を渡してください。
代替手段: esm.town から直接配信する
すべての 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"のような bare specifier はブラウザでは解決できません。バージョンを固定した上で esm.sh などの CDN からインポートしてください:import { createRoot } from "https://esm.sh/react-dom@18.2.0/client";クライアントコード内で bare specifier を使いたい場合は、HTML 内に import map を定義する方法もあります。
この仕組みは React、バニラ DOM スクリプト、canvas ゲームループ、Alpine、htmx など、あらゆるクライアントコードに共通して適用されます。異なるのはインポートの書き方だけです。依存関係のないシンプルな .ts モジュールであれば、CDN から何かを読み込む必要すらありません。
React 固有の注意事項
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 skill を参照してください。
やってはいけないこと
-
インラインの
<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 にパイプし、ページを読み込んでからログを確認してください。この両方を確認せずに変更完了と報告しないでください。
原文(English)を表示
Client-side JavaScript
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).
Serving a module
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.
Alternative: serve directly from esm.town
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.
How imports resolve in the browser
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.
React specifics
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.
What not to do
- No app logic in inline
<script>blobs or template-string HTML. Put client code in real.ts/.tsxfiles so it's typed, linted, and reviewable. A few lines of inline bootstrap are fine; the app is not. - No bundler / build command. There is no build step to add.
serveStaticfrom Hono does not work on Val Town — useserveFile.
Verifying changes
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 による自動翻訳です。