|
| 1 | +# Agents |
| 2 | + |
| 3 | +## Repository Overview |
| 4 | + |
| 5 | +Fresh is a web framework for Deno built on Preact. This is a **Deno monorepo** |
| 6 | +with workspace members in `packages/*` and `www/`. |
| 7 | + |
| 8 | +### Packages |
| 9 | + |
| 10 | +- **`packages/fresh/`** (`@fresh/core`): Core framework — routing, rendering, |
| 11 | + islands, build cache, middlewares, and client/server runtime. |
| 12 | +- **`packages/plugin-vite/`** (`@fresh/plugin-vite`): Vite integration plugin |
| 13 | + with dev server, SSR/client builds, and HMR. |
| 14 | +- **`packages/init/`** (`@fresh/init`): Project scaffolding |
| 15 | + (`deno create @fresh/init`). |
| 16 | +- **`packages/update/`** (`@fresh/update`): Automated Fresh 1.x to 2.x migration |
| 17 | + tool using ts-morph for AST transforms. |
| 18 | +- **`packages/build-id/`** (`@fresh/build-id`): Build/deployment ID generation. |
| 19 | +- **`packages/plugin-tailwindcss/`** (`@fresh/plugin-tailwind`): TailwindCSS v4 |
| 20 | + plugin. |
| 21 | +- **`packages/plugin-tailwindcss-v3/`** (`@fresh/plugin-tailwind-v3`): Legacy |
| 22 | + TailwindCSS v3 plugin. |
| 23 | +- **`packages/examples/`** (`@fresh/examples`): Example components for tests. |
| 24 | + |
| 25 | +### Other directories |
| 26 | + |
| 27 | +- **`www/`**: Documentation website (fresh.deno.dev), built with Fresh + Vite + |
| 28 | + Tailwind. Has its own routes, islands, and vite.config.ts. |
| 29 | +- **`docs/`**: Markdown documentation organized by version (`latest/`, `1.x/`, |
| 30 | + `canary/`). |
| 31 | +- **`tools/`**: `release.ts` (version bumping), `check_docs.ts` (doc |
| 32 | + validation), `check_links.ts` (link checker). |
| 33 | +- **`vendor/`**: Vendored dependencies (`"vendor": true` in deno.json). |
| 34 | + |
| 35 | +## Git Workflow |
| 36 | + |
| 37 | +- To check out a PR branch, use `gh pr checkout <pr-number>`. Do not set up |
| 38 | + remotes manually. |
| 39 | +- Always run `deno fmt` before pushing. |
| 40 | +- Do not commit `deno.lock` changes unless the PR is specifically about updating |
| 41 | + dependencies. Lockfile diffs tend to be noisy and environment-specific. |
| 42 | +- **Never amend commits or force push.** Always create new commits. |
| 43 | + |
| 44 | +## Development |
| 45 | + |
| 46 | +- Run `deno task ok` before pushing — it runs the full local CI check (fmt, |
| 47 | + lint, type check, tests). |
| 48 | +- Run `deno install` if you get missing dependency errors. |
| 49 | +- Tests: `deno task test` (all tests, parallel). Tests use `@std/expect` for |
| 50 | + assertions and `linkedom` for DOM testing. |
| 51 | +- JSX is configured in "precompile" mode with Preact as the import source. |
| 52 | + |
| 53 | +### Lockfile quirks |
| 54 | + |
| 55 | +The lockfile contains remote specifiers pointing to `refs/heads/main` (e.g. |
| 56 | +`raw.githubusercontent.com/.../refs/heads/main/...`). These hashes go stale when |
| 57 | +upstream pushes. When that happens, manually update the hash in `deno.lock` |
| 58 | +since `deno cache --reload` cannot fix it (see |
| 59 | +https://github.com/denoland/deno/issues/32991). |
| 60 | + |
| 61 | +## Architecture |
| 62 | + |
| 63 | +### Request lifecycle |
| 64 | + |
| 65 | +1. `App.handler()` receives an HTTP request (`app.ts`) |
| 66 | +2. URL is parsed and normalized (double slashes removed) |
| 67 | +3. `UrlPatternRouter.match()` finds the matching route — static routes are |
| 68 | + checked first via direct `Map` lookup, then dynamic routes via `URLPattern` |
| 69 | +4. A `Context` is created with request, params, and build cache |
| 70 | +5. The middleware chain executes (built backwards as nested closures) |
| 71 | +6. `ctx.render()` composes layouts and app wrapper around the page component |
| 72 | +7. Preact's `renderToString()` generates HTML, with option hooks detecting |
| 73 | + islands along the way |
| 74 | +8. `FreshScripts` component emits the inline boot script with island imports and |
| 75 | + serialized props |
| 76 | +9. Response is returned with HTML and `Link` modulepreload headers |
| 77 | + |
| 78 | +### Island architecture |
| 79 | + |
| 80 | +Islands are interactive Preact components that hydrate on the client while the |
| 81 | +rest of the page stays static HTML. |
| 82 | + |
| 83 | +**Server side** (`runtime/server/preact_hooks.ts`): |
| 84 | + |
| 85 | +- Preact's diff hook intercepts every VNode during SSR |
| 86 | +- When a component exists in `buildCache.islandRegistry`, it's wrapped in HTML |
| 87 | + comment markers: `<!--frsh:island:NAME:PROPSIDX:KEY-->...<!--/frsh:island-->` |
| 88 | +- Island props are collected into a `RenderState.islandProps[]` array |
| 89 | +- JSX element props become **slots** — stored in `<template>` elements and |
| 90 | + replaced with symbolic references |
| 91 | + |
| 92 | +**Client side** (`runtime/client/reviver.ts`): |
| 93 | + |
| 94 | +- The `boot()` function is called from an inline `<script type="module">` |
| 95 | +- DOM is walked to find `<!--frsh:island:...-->` comment markers |
| 96 | +- Props are deserialized with custom handlers: signals become reactive, slots |
| 97 | + become VNode references |
| 98 | +- Each island is hydrated via `render(h(component, props), container)` using |
| 99 | + `scheduler.postTask()` for non-blocking hydration |
| 100 | + |
| 101 | +### Routing |
| 102 | + |
| 103 | +Filesystem paths are converted to URL patterns (`router.ts`, `fs_routes.ts`): |
| 104 | + |
| 105 | +- `/routes/blog/[id].tsx` becomes `/blog/:id` |
| 106 | +- `/routes/blog/[...rest].tsx` becomes `/blog/:rest*` |
| 107 | +- `/routes/(group)/page.tsx` becomes `/page` (groups are transparent) |
| 108 | +- `/routes/[[id]].tsx` becomes `/:id?` (optional segment) |
| 109 | + |
| 110 | +Routes form a **segment tree** (`segments.ts`) where each level accumulates |
| 111 | +middlewares, layouts, and error handlers. When a route matches, the tree is |
| 112 | +walked from root to leaf to build the full middleware chain. |
| 113 | + |
| 114 | +### Build system |
| 115 | + |
| 116 | +Two build paths exist: |
| 117 | + |
| 118 | +- **esbuild-based** (`dev/builder.ts`, `dev/esbuild.ts`): The native Fresh |
| 119 | + builder. Discovers islands, creates entry points per island + a |
| 120 | + `fresh-runtime` entry, bundles with esbuild-wasm (splitting, tree-shaking, ESM |
| 121 | + output). Output goes to `/_fresh/js/{BUILD_ID}/`. |
| 122 | +- **Vite-based** (`plugin-vite/`): Uses Vite's environment API for dual |
| 123 | + client/SSR builds. Provides the same island discovery and bundling through |
| 124 | + Vite's plugin system with HMR in dev. |
| 125 | + |
| 126 | +Both produce: separate island bundles, a client runtime entry, static assets |
| 127 | +with content hashing, and a BUILD_ID for cache busting. |
| 128 | + |
| 129 | +Build caches come in two flavors: |
| 130 | + |
| 131 | +- `MemoryBuildCache` / `DiskBuildCache` for development (live rebuilds) |
| 132 | +- `ProdBuildCache` for production (snapshot-based, read from `_fresh/`) |
| 133 | + |
| 134 | +### Partials |
| 135 | + |
| 136 | +`<Partial>` components enable incremental page updates without full reloads. |
| 137 | +They are wrapped with markers (`<!--frsh:partial:{name}:{mode}:{key}-->`) and |
| 138 | +support `replace`, `append`, and `prepend` modes. Elements with `f-client-nav` |
| 139 | +enable client-side navigation that fetches and swaps partials instead of full |
| 140 | +page loads. |
| 141 | + |
| 142 | +## CI |
| 143 | + |
| 144 | +CI runs on every PR against `main` across a matrix of Deno v2.x + canary on |
| 145 | +macOS, Windows, and Ubuntu. Steps: |
| 146 | + |
| 147 | +1. `deno install` |
| 148 | +2. `deno fmt --check` (Ubuntu + v2.x only) |
| 149 | +3. `deno lint` (Ubuntu + v2.x only) |
| 150 | +4. Spell-check via `typos` (Ubuntu + v2.x only) |
| 151 | +5. `deno task check:types` (all platforms) |
| 152 | +6. `deno task test` (all platforms) |
| 153 | +7. `deno task check:docs` (all platforms) |
| 154 | +8. `deno task build-www` (Ubuntu + v2.x only) |
| 155 | + |
| 156 | +Publishing to JSR happens automatically on push to `main` via `deno publish`. |
0 commit comments