From 0df4341a73f9ccef07b5b00142ce999c3d9a0da4 Mon Sep 17 00:00:00 2001 From: Steve Krouse Date: Wed, 29 Jul 2026 22:09:52 -0400 Subject: [PATCH 1/4] Teach versioned immutable asset caching in the UI skills client-side-js, http-endpoints, and react-ui now teach the versionedAssets pattern from std/utils as the default way to serve client modules: the shell stamps /v/ asset URLs, serve() answers them with Cache-Control: immutable (302 for stale versions), and publishing invalidates instantly. Measured: repeat visits 665ms -> 157ms with zero asset requests. Co-Authored-By: Claude Fable 5 --- .changeset/versioned-assets-skills.md | 5 +++++ plugin/skills/client-side-js/SKILL.md | 26 ++++++++++++++++++++++++++ plugin/skills/http-endpoints/SKILL.md | 10 ++++++++-- plugin/skills/react-ui/SKILL.md | 20 ++++++++++++++++++++ 4 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 .changeset/versioned-assets-skills.md diff --git a/.changeset/versioned-assets-skills.md b/.changeset/versioned-assets-skills.md new file mode 100644 index 0000000..b911f8f --- /dev/null +++ b/.changeset/versioned-assets-skills.md @@ -0,0 +1,5 @@ +--- +"@valtown/skills": patch +--- + +Teach the versioned-immutable asset-caching pattern (`versionedAssets` from `std/utils`) as the default way to serve client modules, in the `client-side-js`, `http-endpoints`, and `react-ui` skills. The HTML shell stamps `/v/...` asset URLs, `serve()` answers them with `Cache-Control: immutable` (stale versions 302 to current), and publishing the val bumps the version so invalidation is instant. Measured: repeat visits 665ms → 157ms with zero asset requests. diff --git a/plugin/skills/client-side-js/SKILL.md b/plugin/skills/client-side-js/SKILL.md index f4a0b15..2c08724 100644 --- a/plugin/skills/client-side-js/SKILL.md +++ b/plugin/skills/client-side-js/SKILL.md @@ -41,6 +41,32 @@ 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. +## Default: versioned, immutably cached modules + +Served this way, every page load refetches and re-transpiles every module. The +default pattern adds version-stamped URLs with immutable caching on top — +measured on a 3-module React app, repeat visits went **665ms → 157ms with zero +asset requests**: + +```ts +import { versionedAssets } from "https://esm.town/v/std/utils/index.ts"; + +const assets = versionedAssets(); + +// current version → served with Cache-Control: immutable; stale version → 302 +app.get("/v*", (c) => assets.serve(c.req.raw)); +``` + +In the HTML shell, stamp the entry module with `assets.url("/frontend/index.tsx")`, +which returns `/v42/frontend/index.tsx` (42 = the val's current version). Relative +imports resolve under the same `/v42/` prefix, so the whole client module graph is +version-addressed automatically — only the entry needs stamping. + +Invalidation is automatic: publishing the val bumps its version, the never-cached +shell stamps the new prefix, and browsers refetch each asset exactly once; requests +for old versions 302 to the current one. Paths without a `/v/` prefix fall +through to plain uncached `serveFile`, so unstamped URLs keep working. + ### Alternative: serve directly from esm.town Every val file already has a public esm.town URL that transpiles on demand, so you diff --git a/plugin/skills/http-endpoints/SKILL.md b/plugin/skills/http-endpoints/SKILL.md index 1604503..7521dd6 100644 --- a/plugin/skills/http-endpoints/SKILL.md +++ b/plugin/skills/http-endpoints/SKILL.md @@ -25,13 +25,19 @@ When using Hono, export `app.fetch` (not `app`): ```ts import { Hono } from "npm:hono"; -import { parseVal, serveFile } from "https://esm.town/v/std/utils/index.ts"; +import { parseVal, serveFile, versionedAssets } from "https://esm.town/v/std/utils/index.ts"; const app = new Hono(); +const assets = versionedAssets(); app.get("/", (c) => c.text("hello")); -// Serve all frontend files, transpiled, with correct content types +// Serve frontend files versioned + immutably cached. The HTML shell stamps +// asset URLs with assets.url("/frontend/index.tsx") → "/v42/frontend/index.tsx"; +// publishing bumps the version, so browsers refetch each asset exactly once. +app.get("/v*", (c) => assets.serve(c.req.raw)); + +// Unstamped fallback, transpiled, with correct content types (e.g. the favicon) app.get("/frontend/**/*", (c) => serveFile(c.req.path)); // View source redirect diff --git a/plugin/skills/react-ui/SKILL.md b/plugin/skills/react-ui/SKILL.md index 4cfea9b..c160730 100644 --- a/plugin/skills/react-ui/SKILL.md +++ b/plugin/skills/react-ui/SKILL.md @@ -36,6 +36,26 @@ Then use Tailwind classes directly in JSX: Avoid inline `