{"id":7639,"plugin_id":"plugin_asdk_app_6a5e7ac6ddf881919de226cb7506ef57","kind":"skill","collection_source":null,"comparison_source":null,"observed_at":"2026-09-30T22:50:58.071Z","digest":"1be3fe68fb9c5bdd9aaca1d715c91150c3da9975770f7147a2fd3d19a2b1676a","against":null,"payload":{"name":"client-side-js","description":"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.","included_files":[],"skill_md_contents":"---\nname: client-side-js\ndescription: 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.\n---\n\n# Client-side JavaScript\n\nVal Town has **no build step and no bundler**. A client-side module is just a file\nin your val that you serve over HTTP; Val Town transpiles it per request. You point\na `<script type=\"module\">` at a route that returns the file, and the browser runs\nit. There is nothing to configure (no webpack/vite/esbuild).\n\n## Serving a module\n\n`serveFile` from `std/utils` reads a file and serves it with the correct\n`Content-Type`. For `.ts`, `.tsx`, and `.jsx` it **transpiles to JavaScript** —\nstrips types, compiles JSX — and serves `text/javascript`. You serve the source\nfile; the browser receives runnable JS.\n\n```ts\nimport { serveFile } from \"https://esm.town/v/std/utils/index.ts\";\n\n// in any HTTP handler — serve a client module at some URL path\napp.get(\"/app.tsx\", (c) => serveFile(\"/app.tsx\"));\n```\n\nThen load it from your HTML:\n\n```html\n<script type=\"module\" src=\"/app.tsx\"></script>\n```\n\nThe path you serve at and the file's location are up to you. A common shortcut is a\nwildcard that serves a whole directory of modules and assets:\n\n```ts\napp.get(\"/client/**/*\", (c) => serveFile(c.req.path));\n```\n\n`serveFile` defaults to the current val. If you call it from a non-entrypoint file\nand paths don't resolve, pass `import.meta.url` as the second argument.\n\n## Default: versioned, immutably cached modules\n\n`serveImmutableFile` makes your val's frontend faster by letting browsers cache\nfiles immutably; publishing bumps the val's version, which invalidates\nautomatically. Measured: repeat visits **665ms → 157ms with zero asset requests**.\n\n```ts\nimport { immutableFileUrl, serveImmutableFile } from \"https://esm.town/v/std/utils/index.ts\";\n\napp.get(\"/__immutable/*\", (c) => serveImmutableFile(c.req.path));\n```\n\nIn the never-cached HTML shell, stamp the entry module:\n`immutableFileUrl(\"/frontend/index.tsx\")` → `/__immutable/42/frontend/index.tsx`\n(42 = the val's current version). Relative imports resolve under the same prefix,\nso only the entry needs stamping — one route and one stamped URL cover the whole\nclient graph.\n\n- Old-version URLs 404 after a publish (like Next.js build assets); a reload\n  picks up the new version.\n- Retrofitting an existing val without touching its shell? Also point its old\n  file route at `serveImmutableFile` — bare paths then 302 into versioned space,\n  at one redirect per page view.\n\n### Alternative: serve directly from esm.town\n\nEvery val file already has a public esm.town URL that transpiles on demand, so you\ncan skip `serveFile` and point a script straight at it:\n\n```html\n<script type=\"module\" src=\"https://esm.town/v/youruser/yourval/app.tsx\"></script>\n```\n\n`serveFile` is usually preferred because the module is served same-origin from a\npath you control, and you don't have to hardcode your own val URL.\n\n## How imports resolve in the browser\n\nThe transpiler does not bundle or rewrite imports — it only strips types and JSX.\nSo every import in a client module must be something the **browser** can fetch as a\nURL:\n\n- **Local imports need explicit extensions.** `import { x } from \"./util.ts\"`\n  resolves to `/util.ts` (or relative to the served path) and must be served too —\n  by the same route or a wildcard. Omitting the extension (`./util`) 404s.\n- **Third-party deps need full ESM URLs.** Bare specifiers like `import React from\n  \"react\"` don't resolve in the browser. Import from a CDN such as esm.sh, with\n  versions pinned:\n\n  ```ts\n  import { createRoot } from \"https://esm.sh/react-dom@18.2.0/client\";\n  ```\n\n  An [import map](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap)\n  in the HTML is an option if you want bare specifiers in client code.\n\nThe same model works for any client code — React, vanilla DOM scripts, a canvas\ngame loop, Alpine, htmx. Only the imports differ; for a plain `.ts` module with no\ndependencies there's nothing to load from a CDN at all.\n\n## React specifics\n\nPin all React-family imports to the same version (18.2.0) and pass\n`?deps=react@18.2.0,react-dom@18.2.0` on libraries that depend on React. Mismatched\ncopies cause `Cannot read properties of null (reading 'useState')`. See the\n`react-ui` skill for JSX and styling conventions.\n\n## What not to do\n\n- **No app logic in inline `<script>` blobs or template-string HTML.** Put client\n  code in real `.ts`/`.tsx` files so it's typed, linted, and reviewable. A few lines\n  of inline bootstrap are fine; the app is not.\n- **No bundler / build command.** There is no build step to add.\n- **`serveStatic` from Hono does not work** on Val Town — use `serveFile`.\n\n## Verifying changes\n\nFetch the module's URL (e.g. `/app.tsx`) and confirm it returns `text/javascript`,\nnot HTML or an error. Add `https://esm.town/v/std/catch` to the HTML shell to pipe\nbrowser errors into `get_logs`, then load the page and check the logs. Don't report\nthe change as done without both.\n"},"changes":[],"summary":"First saved snapshot. No earlier version is available for comparison.","summary_kind":"deterministic","summary_metadata":{}}