Skip to content

Commit 924fdd4

Browse files
bartlomiejuclaude
andcommitted
merge: resolve conflict with main, keep both tests
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2 parents cd13340 + 14f8955 commit 924fdd4

22 files changed

Lines changed: 544 additions & 14 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ jobs:
3939

4040
steps:
4141
- name: Checkout repo
42-
uses: actions/checkout@v4
42+
uses: actions/checkout@v6
4343

4444
- name: Setup Deno
4545
uses: denoland/setup-deno@v2

.github/workflows/post_publish.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,18 @@ jobs:
1111
if: github.repository == 'denoland/fresh'
1212
steps:
1313
- name: Checkout repo
14-
uses: actions/checkout@v4
14+
uses: actions/checkout@v6
1515

1616
- name: Authenticate with Google Cloud
17-
uses: google-github-actions/auth@v2
17+
uses: google-github-actions/auth@v3
1818
with:
1919
project_id: denoland
2020
credentials_json: ${{ secrets.GCP_SA_KEY }}
2121
export_environment_variables: true
2222
create_credentials_file: true
2323

2424
- name: Setup gcloud
25-
uses: google-github-actions/setup-gcloud@v2
25+
uses: google-github-actions/setup-gcloud@v3
2626
with:
2727
project_id: denoland
2828

.github/workflows/publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
if: github.repository_owner == 'denoland'
1717

1818
steps:
19-
- uses: actions/checkout@v4
19+
- uses: actions/checkout@v6
2020

2121
- name: Install Deno
2222
uses: denoland/setup-deno@v2

docs/latest/advanced/partials.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,66 @@ Partial updates can be animated using the browser's
208208
alongside `f-client-nav` to enable smooth animated transitions between pages
209209
with zero JavaScript animation code.
210210

211+
## Loading indicators
212+
213+
When a partial request is in flight, you may want to show a loading spinner or
214+
disable a button. Fresh supports this through the `_freshIndicator` property.
215+
216+
Attach an object with a `value` property to any element that triggers a partial
217+
navigation. Fresh will set `value` to `true` when the request starts and back to
218+
`false` when it completes (or fails).
219+
220+
```tsx
221+
import { useSignal } from "@preact/signals";
222+
223+
function NavLink() {
224+
const loading = useSignal(false);
225+
226+
return (
227+
<a
228+
href="/next-page"
229+
f-partial="/partials/next-page"
230+
ref={(el) => {
231+
if (el) el._freshIndicator = loading;
232+
}}
233+
>
234+
{loading.value ? "Loading..." : "Go"}
235+
</a>
236+
);
237+
}
238+
```
239+
240+
This works for links, forms, and submit buttons. For form submissions, Fresh
241+
checks the submitter element (e.g. the clicked button) first, then falls back to
242+
the form element itself. This lets you show per-button indicators when a form
243+
has multiple submit buttons.
244+
245+
```tsx
246+
import { useSignal } from "@preact/signals";
247+
248+
function MyForm() {
249+
const saving = useSignal(false);
250+
251+
return (
252+
<form action="/save" f-partial="/partials/save">
253+
{/* indicator is on the button, not the form */}
254+
<button
255+
type="submit"
256+
ref={(el) => {
257+
if (el) el._freshIndicator = saving;
258+
}}
259+
>
260+
{saving.value ? "Saving..." : "Save"}
261+
</button>
262+
</form>
263+
);
264+
}
265+
```
266+
267+
> [info]: Any object with a mutable `value` property works — Preact signals are
268+
> the most convenient choice because they automatically re-render the component
269+
> when the value changes.
270+
211271
## Bypassing or disabling Partials
212272

213273
If you want to exempt a particular element from triggering a partial request

docs/latest/concepts/app.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,23 @@ With `basePath: "/my-app"`, a route registered at `/about` will respond to
3737
mounted alongside other apps. The base path is available in handlers via
3838
`ctx.config.basePath`.
3939

40+
### Reverse proxy support
41+
42+
When running behind a reverse proxy (nginx, Caddy, etc.), set `trustProxy` to
43+
make `ctx.url` reflect the client-facing URL instead of the internal one:
44+
45+
```ts
46+
const app = new App({ trustProxy: true });
47+
```
48+
49+
With this enabled, Fresh reads `X-Forwarded-Proto` and `X-Forwarded-Host`
50+
headers and rewrites `ctx.url` accordingly. For example, if your proxy
51+
terminates TLS and forwards `X-Forwarded-Proto: https`, `ctx.url.protocol` will
52+
be `https:` instead of `http:`.
53+
54+
> [warn]: Only enable `trustProxy` when your app is actually behind a trusted
55+
> reverse proxy. Untrusted clients could otherwise spoof these headers.
56+
4057
All items are applied from top to bottom. This means that when you defined a
4158
middleware _after_ a `.get()` handler, it won't be included.
4259

packages/fresh/src/app.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ export class App<State> {
190190
root: ".",
191191
basePath: config.basePath ?? "",
192192
mode: config.mode ?? "production",
193+
trustProxy: config.trustProxy ?? false,
193194
};
194195
}
195196

@@ -392,6 +393,8 @@ export class App<State> {
392393
this.#onError,
393394
);
394395

396+
const trustProxy = this.config.trustProxy;
397+
395398
return async (
396399
req: Request,
397400
conn: Deno.ServeHandlerInfo = DEFAULT_CONN_INFO,
@@ -400,6 +403,18 @@ export class App<State> {
400403
// Prevent open redirect attacks
401404
url.pathname = url.pathname.replace(/\/+/g, "/");
402405

406+
// Apply X-Forwarded-* headers when behind a reverse proxy
407+
if (trustProxy) {
408+
const proto = req.headers.get("x-forwarded-proto");
409+
if (proto) {
410+
url.protocol = proto + ":";
411+
}
412+
const host = req.headers.get("x-forwarded-host");
413+
if (host) {
414+
url.host = host;
415+
}
416+
}
417+
403418
const method = req.method.toUpperCase() as Method;
404419
const matched = router.match(method, url);
405420
let { params, pattern, item: handler, methodMatch } = matched;

packages/fresh/src/build_cache.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export interface FileSnapshot {
1111
filePath: string;
1212
hash: string | null;
1313
contentType: string;
14+
immutable?: boolean;
1415
}
1516

1617
export interface BuildSnapshot<State> {
@@ -28,6 +29,7 @@ export interface StaticFile {
2829
contentType: string;
2930
readable: ReadableStream<Uint8Array> | Uint8Array;
3031
close(): void;
32+
immutable?: boolean;
3133
}
3234

3335
// deno-lint-ignore no-explicit-any
@@ -87,6 +89,7 @@ export class ProdBuildCache<State> implements BuildCache<State> {
8789
size: stat.size,
8890
readable: file.readable,
8991
close: () => file.close(),
92+
immutable: info.immutable,
9093
};
9194
}
9295
}

packages/fresh/src/config.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,16 @@ export interface FreshConfig {
1111
* The mode Fresh can run in.
1212
*/
1313
mode?: "development" | "production";
14+
/**
15+
* When enabled, Fresh will respect `X-Forwarded-Proto` and
16+
* `X-Forwarded-Host` headers to construct `ctx.url`. Enable
17+
* this when running behind a reverse proxy.
18+
*
19+
* Only enable `trustProxy` when your app is actually behind a trusted
20+
* reverse proxy. Untrusted clients could otherwise spoof these headers.
21+
* @default false
22+
*/
23+
trustProxy?: boolean;
1424
}
1525

1626
/**
@@ -27,6 +37,15 @@ export interface ResolvedFreshConfig {
2737
* The mode Fresh can run in.
2838
*/
2939
mode: "development" | "production";
40+
/**
41+
* When enabled, Fresh will respect `X-Forwarded-Proto` and
42+
* `X-Forwarded-Host` headers to construct `ctx.url`. Enable
43+
* this when running behind a reverse proxy.
44+
*
45+
* Only enable `trustProxy` when your app is actually behind a trusted
46+
* reverse proxy. Untrusted clients could otherwise spoof these headers.
47+
*/
48+
trustProxy: boolean;
3049
}
3150

3251
export function parseDirPath(

packages/fresh/src/dev/dev_build_cache.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,7 @@ export interface PendingStaticFile {
438438
pathname: string;
439439
filePath: string;
440440
hash: string | null;
441+
immutable?: boolean;
441442
}
442443

443444
export async function writeCompiledEntry(outDir: string) {
@@ -561,7 +562,13 @@ export async function prepareStaticFile(
561562
item: PendingStaticFile,
562563
outDir: string,
563564
): Promise<
564-
{ name: string; hash: string; filePath: string; contentType: string }
565+
{
566+
name: string;
567+
hash: string;
568+
filePath: string;
569+
contentType: string;
570+
immutable?: boolean;
571+
}
565572
> {
566573
const file = await Deno.open(item.filePath);
567574
const hash = item.hash ? item.hash : await hashContent(file.readable);
@@ -576,6 +583,7 @@ export async function prepareStaticFile(
576583
: item.filePath,
577584
),
578585
contentType: getContentType(item.filePath),
586+
immutable: item.immutable,
579587
};
580588
}
581589

packages/fresh/src/middlewares/static_files.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,8 @@ export function staticFiles<T>(): Middleware<T> {
8989
(BUILD_ID === cacheKey ||
9090
url.pathname.startsWith(
9191
`${ctx.config.basePath}/_fresh/js/${BUILD_ID}/`,
92-
))
92+
) ||
93+
file.immutable)
9394
) {
9495
span.setAttribute("fresh.cache", "immutable");
9596
headers.append("Cache-Control", "public, max-age=31536000, immutable");

0 commit comments

Comments
 (0)