Fresh is a web framework for Deno built on Preact. This is a Deno monorepo
with workspace members in packages/* and www/.
packages/fresh/(@fresh/core): Core framework — routing, rendering, islands, build cache, middlewares, and client/server runtime.packages/plugin-vite/(@fresh/plugin-vite): Vite integration plugin with dev server, SSR/client builds, and HMR.packages/init/(@fresh/init): Project scaffolding (deno run -Ar jsr:@fresh/init).packages/update/(@fresh/update): Automated Fresh 1.x to 2.x migration tool using ts-morph for AST transforms.packages/build-id/(@fresh/build-id): Build/deployment ID generation.packages/plugin-tailwindcss/(@fresh/plugin-tailwind): TailwindCSS v4 plugin.packages/plugin-tailwindcss-v3/(@fresh/plugin-tailwind-v3): Legacy TailwindCSS v3 plugin.packages/examples/(@fresh/examples): Example components for tests.
www/: Documentation website (fresh.deno.dev), built with Fresh + Vite + Tailwind. Has its own routes, islands, and vite.config.ts.docs/: Markdown documentation organized by version (latest/,1.x/,canary/).tools/:release.ts(version bumping),check_docs.ts(doc validation),check_links.ts(link checker).vendor/: Vendored dependencies ("vendor": truein deno.json).
- To check out a PR branch, use
gh pr checkout <pr-number>. Do not set up remotes manually. - Always run
deno fmtbefore pushing. - Do not commit
deno.lockchanges unless the PR is specifically about updating dependencies. Lockfile diffs tend to be noisy and environment-specific. - Never amend commits or force push. Always create new commits.
- Run
deno task okbefore pushing — it runs the full local CI check (fmt, lint, type check, tests). - Run
deno installif you get missing dependency errors. - Tests:
deno task test(all tests, parallel). Tests use@std/expectfor assertions andlinkedomfor DOM testing. - JSX is configured in "precompile" mode with Preact as the import source.
The lockfile contains remote specifiers pointing to refs/heads/main (e.g.
raw.githubusercontent.com/.../refs/heads/main/...). These hashes go stale when
upstream pushes. When that happens, manually update the hash in deno.lock
since deno cache --reload cannot fix it (see
denoland/deno#32991).
App.handler()receives an HTTP request (app.ts)- URL is parsed and normalized (double slashes removed)
UrlPatternRouter.match()finds the matching route — static routes are checked first via directMaplookup, then dynamic routes viaURLPattern- A
Contextis created with request, params, and build cache - The middleware chain executes (built backwards as nested closures)
ctx.render()composes layouts and app wrapper around the page component- Preact's
renderToString()generates HTML, with option hooks detecting islands along the way FreshScriptscomponent emits the inline boot script with island imports and serialized props- Response is returned with HTML and
Linkmodulepreload headers
Islands are interactive Preact components that hydrate on the client while the rest of the page stays static HTML.
Server side (runtime/server/preact_hooks.ts):
- Preact's diff hook intercepts every VNode during SSR
- When a component exists in
buildCache.islandRegistry, it's wrapped in HTML comment markers:<!--frsh:island:NAME:PROPSIDX:KEY-->...<!--/frsh:island--> - Island props are collected into a
RenderState.islandProps[]array - JSX element props become slots — stored in
<template>elements and replaced with symbolic references
Client side (runtime/client/reviver.ts):
- The
boot()function is called from an inline<script type="module"> - DOM is walked to find
<!--frsh:island:...-->comment markers - Props are deserialized with custom handlers: signals become reactive, slots become VNode references
- Each island is hydrated via
render(h(component, props), container)usingscheduler.postTask()for non-blocking hydration
Filesystem paths are converted to URL patterns (router.ts, fs_routes.ts):
/routes/blog/[id].tsxbecomes/blog/:id/routes/blog/[...rest].tsxbecomes/blog/:rest*/routes/(group)/page.tsxbecomes/page(groups are transparent)/routes/[[id]].tsxbecomes/:id?(optional segment)
Routes form a segment tree (segments.ts) where each level accumulates
middlewares, layouts, and error handlers. When a route matches, the tree is
walked from root to leaf to build the full middleware chain.
Two build paths exist:
- esbuild-based (
dev/builder.ts,dev/esbuild.ts): The native Fresh builder. Discovers islands, creates entry points per island + afresh-runtimeentry, bundles with esbuild-wasm (splitting, tree-shaking, ESM output). Output goes to/_fresh/js/{BUILD_ID}/. - Vite-based (
plugin-vite/): Uses Vite's environment API for dual client/SSR builds. Provides the same island discovery and bundling through Vite's plugin system with HMR in dev.
Both produce: separate island bundles, a client runtime entry, static assets with content hashing, and a BUILD_ID for cache busting.
Build caches come in two flavors:
MemoryBuildCache/DiskBuildCachefor development (live rebuilds)ProdBuildCachefor production (snapshot-based, read from_fresh/)
<Partial> components enable incremental page updates without full reloads.
They are wrapped with markers (<!--frsh:partial:{name}:{mode}:{key}-->) and
support replace, append, and prepend modes. Elements with f-client-nav
enable client-side navigation that fetches and swaps partials instead of full
page loads.
CI runs on every PR against main across a matrix of Deno v2.x + canary on
macOS, Windows, and Ubuntu. Steps:
deno installdeno fmt --check(Ubuntu + v2.x only)deno lint(Ubuntu + v2.x only)- Spell-check via
typos(Ubuntu + v2.x only) deno task check:types(all platforms)deno task test(all platforms)deno task check:docs(all platforms)deno task build-www(Ubuntu + v2.x only)
Publishing to JSR happens automatically on push to main via deno publish.