htmx-router 2.0.3 → 2.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "htmx-router",
3
- "version": "2.0.3",
3
+ "version": "2.0.5",
4
4
  "description": "A lightweight SSR framework with server+client islands",
5
5
  "keywords": [
6
6
  "htmx",
package/response.d.ts CHANGED
@@ -7,8 +7,54 @@ export type TypedJson<U extends TypedResponse<any>> = U extends TypedResponse<in
7
7
  export declare function json<T>(data: T, init?: ResponseInit): TypedResponse<T>;
8
8
  export declare function redirect(url: string, init?: ResponseInit & {
9
9
  clientOnly?: boolean;
10
+ permanent?: boolean;
10
11
  }): Response;
11
12
  export declare function revalidate(init?: ResponseInit): Response;
12
13
  export declare function refresh(init?: ResponseInit & {
13
14
  clientOnly?: boolean;
14
15
  }): Response;
16
+ /**
17
+ * Handles Entity-Tag based conditional requests by setting cache headers and controlling execution flow.
18
+ * Acts like an assertion that stops execution when the client's ETag matches the current ETag.
19
+ *
20
+ * @param {Request} request - The incoming HTTP request object
21
+ * @param {Headers} headers - The response headers object to modify
22
+ * @param {string} etag - The current ETag value for the resource
23
+ * @param {Object} [options] - Optional caching configuration
24
+ * @param {number} [options.revalidate] - client must revalidate their etag at this interval in seconds
25
+ * @param {boolean} [options.public] - cache visibility scope:
26
+ * - "public": Can be cached by any cache (i.e. Cloudflare)
27
+ * - "private": Can only be cached by private caches (i.e. browser)
28
+ *
29
+ * @throws {Response} `304 Not Modified` Response with the configured headers
30
+ * when the client's If-None-Match header matches the provided ETag
31
+ *
32
+ * @returns {void} if client's etag is stale, allows execution to continue to generate new result
33
+ *
34
+ * @example
35
+ * // Basic usage - will return early if client has current version
36
+ * GuardEntityTag(request, headers, "v1.2.3");
37
+ *
38
+ * @example
39
+ * // With caching options
40
+ * GuardEntityTag(request, headers, "v1.2.3", {
41
+ * revalidate: 3600,
42
+ * scope: "public"
43
+ * });
44
+ *
45
+ * @example
46
+ * // Usage in a handler function
47
+ * function handleRequest(request, headers) {
48
+ * const currentEtag = generateEtag(data);
49
+ *
50
+ * // Will throw 304 if client ETag is fresh
51
+ * GuardEntityTag(request, headers, currentEtag);
52
+ *
53
+ * // This code only runs if client needs updated content
54
+ * return new Response(generateContent(), { headers });
55
+ * }
56
+ */
57
+ export declare function AssertETagStale(request: Request, headers: Headers, etag: string, options?: {
58
+ revalidate?: number;
59
+ public?: boolean;
60
+ }): void;
package/response.js CHANGED
@@ -1,55 +1,131 @@
1
1
  export function text(text, init) {
2
- init ||= {};
3
- init.statusText ||= "ok";
4
- init.status ||= 200;
2
+ init = FillResponseInit(200, "Ok", init);
5
3
  const res = new Response(text, init);
6
4
  res.headers.set("Content-Type", "text/plain");
7
5
  res.headers.set("X-Caught", "true");
8
6
  return res;
9
7
  }
10
8
  export function html(text, init) {
11
- init ||= {};
12
- init.statusText ||= "ok";
13
- init.status ||= 200;
9
+ init = FillResponseInit(200, "Ok", init);
14
10
  const res = new Response(text, init);
15
11
  res.headers.set("Content-Type", "text/html; charset=UTF-8");
16
12
  res.headers.set("X-Caught", "true");
17
13
  return res;
18
14
  }
19
15
  export function json(data, init) {
20
- init ||= {};
21
- init.statusText ||= "ok";
22
- init.status ||= 200;
16
+ init = FillResponseInit(200, "Ok", init);
23
17
  const res = new Response(JSON.stringify(data), init);
24
18
  res.headers.set("Content-Type", "application/json");
25
19
  res.headers.set("X-Caught", "true");
26
20
  return res;
27
21
  }
28
22
  export function redirect(url, init) {
29
- init ||= {};
30
- init.statusText ||= "Temporary Redirect";
31
- init.status ||= 307;
32
- const res = new Response("", init);
23
+ if (init?.permanent)
24
+ init = FillResponseInit(308, "Permanent Redirect", init);
25
+ else
26
+ init = FillResponseInit(307, "Temporary Redirect", init);
27
+ const res = new Response(null, init);
33
28
  if (!init?.clientOnly)
34
29
  res.headers.set("Location", url);
35
30
  res.headers.set("HX-Location", url); // use hx-boost if applicable
31
+ res.headers.set("X-Caught", "true");
36
32
  return res;
37
33
  }
38
34
  export function revalidate(init) {
39
- init ||= {};
40
- init.statusText ||= "ok";
41
- init.status ||= 200;
35
+ init = FillResponseInit(200, "Ok", init);
42
36
  const res = new Response("", init);
43
37
  res.headers.set("HX-Location", "");
38
+ res.headers.set("HX-Replace-Url", "");
39
+ res.headers.set("X-Caught", "true");
44
40
  return res;
45
41
  }
46
42
  export function refresh(init) {
47
- init ||= {};
48
- init.statusText ||= "ok";
49
- init.status ||= 200;
50
- const res = new Response("", init);
51
- if (!init?.clientOnly)
52
- res.headers.set("Refresh", "0"); // fallback
43
+ init = FillResponseInit(200, "Ok", init);
44
+ if (init.clientOnly) {
45
+ const res = new Response(null, init);
46
+ res.headers.set("HX-Refresh", "true");
47
+ res.headers.set("X-Caught", "true");
48
+ return res;
49
+ }
50
+ const res = new Response(`<script>if (document.referrer) location.href = document.referrer;</script>Something went wrong`);
51
+ res.headers.set("Content-Type", "text/html; charset=UTF-8");
53
52
  res.headers.set("HX-Refresh", "true");
53
+ res.headers.set("X-Caught", "true");
54
54
  return res;
55
55
  }
56
+ /**
57
+ * Handles Entity-Tag based conditional requests by setting cache headers and controlling execution flow.
58
+ * Acts like an assertion that stops execution when the client's ETag matches the current ETag.
59
+ *
60
+ * @param {Request} request - The incoming HTTP request object
61
+ * @param {Headers} headers - The response headers object to modify
62
+ * @param {string} etag - The current ETag value for the resource
63
+ * @param {Object} [options] - Optional caching configuration
64
+ * @param {number} [options.revalidate] - client must revalidate their etag at this interval in seconds
65
+ * @param {boolean} [options.public] - cache visibility scope:
66
+ * - "public": Can be cached by any cache (i.e. Cloudflare)
67
+ * - "private": Can only be cached by private caches (i.e. browser)
68
+ *
69
+ * @throws {Response} `304 Not Modified` Response with the configured headers
70
+ * when the client's If-None-Match header matches the provided ETag
71
+ *
72
+ * @returns {void} if client's etag is stale, allows execution to continue to generate new result
73
+ *
74
+ * @example
75
+ * // Basic usage - will return early if client has current version
76
+ * GuardEntityTag(request, headers, "v1.2.3");
77
+ *
78
+ * @example
79
+ * // With caching options
80
+ * GuardEntityTag(request, headers, "v1.2.3", {
81
+ * revalidate: 3600,
82
+ * scope: "public"
83
+ * });
84
+ *
85
+ * @example
86
+ * // Usage in a handler function
87
+ * function handleRequest(request, headers) {
88
+ * const currentEtag = generateEtag(data);
89
+ *
90
+ * // Will throw 304 if client ETag is fresh
91
+ * GuardEntityTag(request, headers, currentEtag);
92
+ *
93
+ * // This code only runs if client needs updated content
94
+ * return new Response(generateContent(), { headers });
95
+ * }
96
+ */
97
+ export function AssertETagStale(request, headers, etag, options) {
98
+ if (options) {
99
+ // default to private, because it's the slightly less worse of the two potential foot guns
100
+ if (options.public)
101
+ headers.append("Cache-Control", "public");
102
+ else
103
+ headers.append("Cache-Control", "private");
104
+ if (options.revalidate !== undefined)
105
+ headers.append("Cache-Control", `max-age=${options.revalidate}`);
106
+ }
107
+ headers.append("Cache-Control", "must-revalidate");
108
+ headers.set("ETag", etag);
109
+ const client = request.headers.get("if-none-match");
110
+ if (client === etag)
111
+ return;
112
+ const res = new Response(null, {
113
+ status: 304, statusText: "Not Modified",
114
+ headers
115
+ });
116
+ res.headers.set("X-Caught", "true");
117
+ throw res;
118
+ }
119
+ /**
120
+ * This is to fix issues with deno
121
+ * When you try and change the statusText on a Response object
122
+ */
123
+ function FillResponseInit(status, statusText, init) {
124
+ if (init === undefined)
125
+ init = {};
126
+ if (init.statusText === undefined)
127
+ init.statusText = statusText;
128
+ if (init.status === undefined)
129
+ init.status = status;
130
+ return init;
131
+ }