htmx-router 2.0.5 → 2.1.1

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.
Files changed (76) hide show
  1. package/{cli → dist/cli}/index.js +0 -0
  2. package/dist/event-source.d.ts +61 -0
  3. package/{event-source.js → dist/event-source.js} +103 -27
  4. package/{internal → dist/internal}/request/server.d.ts +11 -2
  5. package/{internal → dist/internal}/request/server.js +36 -11
  6. package/{internal → dist/internal}/router.d.ts +9 -11
  7. package/{internal → dist/internal}/router.js +6 -15
  8. package/{response.d.ts → dist/response.d.ts} +3 -1
  9. package/{response.js → dist/response.js} +5 -3
  10. package/{router.js → dist/router.js} +10 -6
  11. package/{timer.d.ts → dist/timer.d.ts} +5 -0
  12. package/{timer.js → dist/timer.js} +5 -0
  13. package/package.json +24 -9
  14. package/event-source.d.ts +0 -31
  15. /package/{cli → dist/cli}/config.d.ts +0 -0
  16. /package/{cli → dist/cli}/config.js +0 -0
  17. /package/{cli → dist/cli}/index.d.ts +0 -0
  18. /package/{cookies.d.ts → dist/cookies.d.ts} +0 -0
  19. /package/{cookies.js → dist/cookies.js} +0 -0
  20. /package/{css.d.ts → dist/css.d.ts} +0 -0
  21. /package/{css.js → dist/css.js} +0 -0
  22. /package/{defer.d.ts → dist/defer.d.ts} +0 -0
  23. /package/{defer.js → dist/defer.js} +0 -0
  24. /package/{endpoint.d.ts → dist/endpoint.d.ts} +0 -0
  25. /package/{endpoint.js → dist/endpoint.js} +0 -0
  26. /package/{index.d.ts → dist/index.d.ts} +0 -0
  27. /package/{index.js → dist/index.js} +0 -0
  28. /package/{internal → dist/internal}/client.d.ts +0 -0
  29. /package/{internal → dist/internal}/client.js +0 -0
  30. /package/{internal → dist/internal}/compile/manifest.d.ts +0 -0
  31. /package/{internal → dist/internal}/compile/manifest.js +0 -0
  32. /package/{internal → dist/internal}/component/defer.d.ts +0 -0
  33. /package/{internal → dist/internal}/component/defer.js +0 -0
  34. /package/{internal → dist/internal}/component/head.d.ts +0 -0
  35. /package/{internal → dist/internal}/component/head.js +0 -0
  36. /package/{internal → dist/internal}/component/index.d.ts +0 -0
  37. /package/{internal → dist/internal}/component/index.js +0 -0
  38. /package/{internal → dist/internal}/component/scripts.d.ts +0 -0
  39. /package/{internal → dist/internal}/component/scripts.js +0 -0
  40. /package/{internal → dist/internal}/mount.d.ts +0 -0
  41. /package/{internal → dist/internal}/mount.js +0 -0
  42. /package/{internal → dist/internal}/request/compatibility/node.d.ts +0 -0
  43. /package/{internal → dist/internal}/request/compatibility/node.js +0 -0
  44. /package/{internal → dist/internal}/request/compatibility/vite/connectToWeb.d.ts +0 -0
  45. /package/{internal → dist/internal}/request/compatibility/vite/connectToWeb.js +0 -0
  46. /package/{internal → dist/internal}/request/compatibility/vite/createServerResponse.d.ts +0 -0
  47. /package/{internal → dist/internal}/request/compatibility/vite/createServerResponse.js +0 -0
  48. /package/{internal → dist/internal}/request/compatibility/vite/header-utils.d.ts +0 -0
  49. /package/{internal → dist/internal}/request/compatibility/vite/header-utils.js +0 -0
  50. /package/{internal → dist/internal}/request/index.d.ts +0 -0
  51. /package/{internal → dist/internal}/request/index.js +0 -0
  52. /package/{internal → dist/internal}/util.d.ts +0 -0
  53. /package/{internal → dist/internal}/util.js +0 -0
  54. /package/{navigate.d.ts → dist/navigate.d.ts} +0 -0
  55. /package/{navigate.js → dist/navigate.js} +0 -0
  56. /package/{router.d.ts → dist/router.d.ts} +0 -0
  57. /package/{server.d.ts → dist/server.d.ts} +0 -0
  58. /package/{server.js → dist/server.js} +0 -0
  59. /package/{shell.d.ts → dist/shell.d.ts} +0 -0
  60. /package/{shell.js → dist/shell.js} +0 -0
  61. /package/{status.d.ts → dist/status.d.ts} +0 -0
  62. /package/{status.js → dist/status.js} +0 -0
  63. /package/{util → dist/util}/parameters.d.ts +0 -0
  64. /package/{util → dist/util}/parameters.js +0 -0
  65. /package/{util → dist/util}/path-builder.d.ts +0 -0
  66. /package/{util → dist/util}/path-builder.js +0 -0
  67. /package/{util → dist/util}/route.d.ts +0 -0
  68. /package/{util → dist/util}/route.js +0 -0
  69. /package/{vite → dist/vite}/bundle-splitter.d.ts +0 -0
  70. /package/{vite → dist/vite}/bundle-splitter.js +0 -0
  71. /package/{vite → dist/vite}/client-island.d.ts +0 -0
  72. /package/{vite → dist/vite}/client-island.js +0 -0
  73. /package/{vite → dist/vite}/index.d.ts +0 -0
  74. /package/{vite → dist/vite}/index.js +0 -0
  75. /package/{vite → dist/vite}/router.d.ts +0 -0
  76. /package/{vite → dist/vite}/router.js +0 -0
File without changes
@@ -0,0 +1,61 @@
1
+ type Render = (jsx: JSX.Element) => string;
2
+ /**
3
+ * Helper for Server-Sent-Events, with auto close on SIGTERM and SIGHUP messages
4
+ * Includes a keep alive empty packet sent every 30sec (because Chrome implodes at 120sec, and can be unreliable at 60sec)
5
+ */
6
+ export declare class EventSource<JsxEnabled extends boolean = false> {
7
+ #private;
8
+ readonly response: Response;
9
+ readonly createdAt: number;
10
+ get updatedAt(): number;
11
+ readonly withCredentials: boolean;
12
+ readonly url: string;
13
+ get readyState(): number;
14
+ static CONNECTING: number;
15
+ static OPEN: number;
16
+ static CLOSED: number;
17
+ constructor(request: Request, render: JsxEnabled extends true ? Render : undefined);
18
+ isAborted(): boolean;
19
+ /**
20
+ * For internal use only
21
+ * @deprecated
22
+ */
23
+ _keepAlive(): boolean;
24
+ dispatch(type: string, data: JsxEnabled extends true ? (JSX.Element | string) : string): boolean;
25
+ close(unlink?: boolean): boolean;
26
+ }
27
+ export declare class EventSourceSet<JsxEnabled extends boolean = false> extends Set<EventSource<JsxEnabled>> {
28
+ /** Send update to all EventSources, auto closing failed dispatches */
29
+ dispatch(type: string, data: string): void;
30
+ /** Cull all closed connections */
31
+ cull(): void;
32
+ /** Close all connections */
33
+ closeAll(): void;
34
+ }
35
+ type SharedEventSourceCacheRule = {
36
+ limit: number;
37
+ ttl: number;
38
+ } | {
39
+ limit?: number;
40
+ ttl: number;
41
+ } | {
42
+ limit: number;
43
+ ttl?: number;
44
+ };
45
+ /**
46
+ * DO NOT USE: Experimental
47
+ * @deprecated
48
+ */
49
+ export declare class SharedEventSource<JsxEnabled extends boolean = false> {
50
+ #private;
51
+ constructor(props: {
52
+ cache?: Record<string, SharedEventSourceCacheRule>;
53
+ } & (JsxEnabled extends true ? {
54
+ render: Render;
55
+ } : {}));
56
+ create(request: Request): EventSource<JsxEnabled>;
57
+ dispatch(type: string, data: JsxEnabled extends true ? (JSX.Element | string) : string): void;
58
+ isEmpty(): boolean;
59
+ close(): void;
60
+ }
61
+ export {};
@@ -1,3 +1,4 @@
1
+ var _a;
1
2
  import { ServerOnlyWarning } from "./internal/util.js";
2
3
  ServerOnlyWarning("event-source");
3
4
  // global for easy reuse
@@ -18,8 +19,8 @@ const headers = {
18
19
  export class EventSource {
19
20
  #controller;
20
21
  #signal;
21
- #timer;
22
22
  #state;
23
+ #render;
23
24
  response;
24
25
  // activity timestamps, in unix time for minimal storage
25
26
  // since most use cases won't need the Date object anyway
@@ -34,26 +35,26 @@ export class EventSource {
34
35
  static CONNECTING = 0;
35
36
  static OPEN = 1;
36
37
  static CLOSED = 2;
37
- constructor(request, keepAlive = 30_000) {
38
+ constructor(request, render) {
38
39
  this.#controller = null;
39
- this.#state = EventSource.CONNECTING;
40
+ this.#state = _a.CONNECTING;
41
+ this.#render = render;
40
42
  this.withCredentials = request.mode === "cors";
41
43
  this.url = request.url;
42
44
  this.createdAt = Date.now();
43
45
  this.#updatedAt = 0;
44
46
  // immediate prepare for abortion
45
- const cancel = () => { this.close(); request.signal.removeEventListener("abort", cancel); };
46
- request.signal.addEventListener('abort', cancel);
47
47
  this.#signal = request.signal;
48
- const start = (c) => { this.#controller = c; this.#state = EventSource.OPEN; };
48
+ const cancel = () => { this.close(); this.#signal.removeEventListener("abort", cancel); };
49
+ this.#signal.addEventListener('abort', cancel);
50
+ const start = (c) => { this.#controller = c; this.#state = _a.OPEN; };
49
51
  const stream = new ReadableStream({ start, cancel }, { highWaterMark: 0 });
50
52
  this.response = new Response(stream, { headers });
51
- this.#timer = setInterval(() => this.keepAlive(), keepAlive);
52
- register.add(this);
53
+ keepAlive.add(this);
53
54
  }
54
55
  isAborted() { return this.#signal.aborted; }
55
- sendBytes(chunk, active) {
56
- if (this.#state === EventSource.CLOSED) {
56
+ #sendBytes(chunk, active) {
57
+ if (this.#state === _a.CLOSED) {
57
58
  const err = new Error(`Warn: Attempted to send data on closed stream for: ${this.url}`);
58
59
  console.warn(err);
59
60
  }
@@ -75,14 +76,26 @@ export class EventSource {
75
76
  return false;
76
77
  }
77
78
  }
78
- sendText(chunk, simulated) {
79
- return this.sendBytes(encoder.encode(chunk), simulated);
79
+ #sendText(chunk, simulated) {
80
+ return this.#sendBytes(encoder.encode(chunk), simulated);
80
81
  }
81
- keepAlive() {
82
- return this.sendText("\n\n", false);
82
+ /**
83
+ * For internal use only
84
+ * @deprecated
85
+ */
86
+ _keepAlive() {
87
+ return this.#sendText("\n\n", false);
83
88
  }
84
89
  dispatch(type, data) {
85
- return this.sendText(`event: ${type}\ndata: ${data}\n\n`, true);
90
+ let html;
91
+ if (typeof data === "string")
92
+ html = data;
93
+ else {
94
+ if (!this.#render)
95
+ throw new Error(`Cannot render to JSX when no renderer provided during class initialization`);
96
+ html = this.#render(data);
97
+ }
98
+ return this.#sendText(`event: ${type}\ndata: ${html}\n\n`, true);
86
99
  }
87
100
  close(unlink = true) {
88
101
  if (this.#controller) {
@@ -95,18 +108,16 @@ export class EventSource {
95
108
  this.#controller = null;
96
109
  }
97
110
  // Cleanup
98
- if (this.#timer)
99
- clearInterval(this.#timer);
100
- if (unlink)
101
- register.delete(this);
111
+ keepAlive.delete(this);
102
112
  // was already closed
103
- if (this.#state === EventSource.CLOSED)
113
+ if (this.#state === _a.CLOSED)
104
114
  return false;
105
115
  // Mark closed
106
- this.#state = EventSource.CLOSED;
116
+ this.#state = _a.CLOSED;
107
117
  return true;
108
118
  }
109
119
  }
120
+ _a = EventSource;
110
121
  export class EventSourceSet extends Set {
111
122
  /** Send update to all EventSources, auto closing failed dispatches */
112
123
  dispatch(type, data) {
@@ -133,13 +144,78 @@ export class EventSourceSet extends Set {
133
144
  this.clear();
134
145
  }
135
146
  }
147
+ /**
148
+ * DO NOT USE: Experimental
149
+ * @deprecated
150
+ */
151
+ export class SharedEventSource {
152
+ #pool;
153
+ #render;
154
+ #cache;
155
+ #rules;
156
+ constructor(props) {
157
+ this.#pool = new EventSourceSet();
158
+ this.#render = props?.render || undefined;
159
+ this.#cache = {};
160
+ this.#rules = {};
161
+ }
162
+ create(request) {
163
+ const source = new EventSource(request, this.#render);
164
+ const buffer = [];
165
+ for (const name in this.#cache)
166
+ buffer.push(...this.#cache[name].map(x => ({ t: name, x })));
167
+ buffer.sort((a, b) => b.x.t - a.x.t);
168
+ for (const e of buffer)
169
+ source.dispatch(e.t, e.x.s);
170
+ return source;
171
+ }
172
+ dispatch(type, data) {
173
+ let html;
174
+ if (typeof data === "string")
175
+ html = data;
176
+ else {
177
+ if (!this.#render)
178
+ throw new Error(`Cannot render to JSX when no renderer provided during class initialization`);
179
+ html = this.#render(data);
180
+ }
181
+ this.#pool.dispatch(type, html);
182
+ // Cache management
183
+ const rule = this.#rules[type];
184
+ if (!rule)
185
+ return;
186
+ const t = Date.now();
187
+ this.#cache[type] ||= [];
188
+ const queue = this.#cache[type];
189
+ queue.push({ t, s: html });
190
+ // Purge Cache
191
+ let i = rule.limit ? Math.max(0, queue.length - rule.limit) : 0;
192
+ if (rule.ttl) {
193
+ const window = t - rule.ttl;
194
+ for (; i < queue.length; i++)
195
+ if (queue[i].t > window)
196
+ break;
197
+ }
198
+ this.#cache[type] = queue.slice(i);
199
+ }
200
+ isEmpty() {
201
+ return this.#pool.size < 1;
202
+ }
203
+ close() { this.#pool.closeAll(); }
204
+ }
136
205
  // Auto close all SSE streams when shutdown requested
137
206
  // Without this graceful shutdowns will hang indefinitely
138
- const register = new EventSourceSet();
139
- function CloseAll() {
140
- register.closeAll();
141
- }
207
+ const keepAlive = new Set();
208
+ const interval = setInterval(() => {
209
+ for (const e of keepAlive) {
210
+ if (e.readyState === EventSource.CLOSED) {
211
+ keepAlive.delete(e);
212
+ continue;
213
+ }
214
+ e._keepAlive();
215
+ }
216
+ }, 10_000);
217
+ function Shutdown() { clearInterval(interval); }
142
218
  if (process) {
143
- process.on('SIGTERM', CloseAll);
144
- process.on('SIGTERM', CloseAll);
219
+ process.on('SIGTERM', Shutdown);
220
+ process.on('SIGHUP', Shutdown);
145
221
  }
@@ -3,25 +3,34 @@ import { GenericContext } from "../router.js";
3
3
  import { RouterModule } from "./index.js";
4
4
  export type Config = {
5
5
  build: () => Promise<RouterModule> | Promise<RouterModule>;
6
- render: GenericContext["render"];
6
+ render: (res: JSX.Element) => string;
7
7
  viteDevServer: ViteDevServer | null;
8
+ headers?: Headers;
8
9
  poweredBy?: boolean;
9
10
  timers?: boolean;
10
11
  };
11
12
  type ServerBindType = "pre" | "post";
12
13
  type ServerBind = (ctx: GenericContext) => Promise<Response | null> | Response | null;
14
+ type Transformer = (ctx: GenericContext, res: Response) => Promise<Response | void> | Response | void;
13
15
  export declare class HtmxRouterServer {
14
16
  #private;
15
17
  readonly vite: Config["viteDevServer"];
16
18
  readonly render: Config["render"];
17
19
  readonly build: Config["build"];
18
- readonly poweredBy: boolean;
20
+ readonly headers: Headers;
19
21
  readonly timers: boolean;
20
22
  constructor(config: Config);
21
23
  /**
22
24
  * Add a middleware to resolve requests before/after the route tree attempts to resolve
23
25
  */
24
26
  use(type: ServerBindType, binding: ServerBind): void;
27
+ /**
28
+ * Be careful with your transformers, there is no error unwrapping built in
29
+ * @param binding
30
+ * @returns
31
+ */
32
+ useTransform(binding: Transformer): void;
33
+ transform(ctx: GenericContext, res: Response): Promise<Response>;
25
34
  resolve<T extends boolean>(request: Request, resolve404?: T): Promise<T extends true ? Response : (Response | null)>;
26
35
  /**
27
36
  * Use the top level error handler in the route tree to render an error
@@ -4,6 +4,7 @@ import { connectToWeb } from "./compatibility/vite/connectToWeb.js";
4
4
  import { GenericContext } from "../router.js";
5
5
  import { NodeAdaptor } from "./compatibility/node.js";
6
6
  import { MakeStatus } from "../../status.js";
7
+ import { redirect } from "../../response.js";
7
8
  function UrlCleaner({ url }) {
8
9
  const i = url.pathname.lastIndexOf("/");
9
10
  if (i === 0)
@@ -11,26 +12,29 @@ function UrlCleaner({ url }) {
11
12
  if (i !== url.pathname.length - 1)
12
13
  return null;
13
14
  url.pathname = url.pathname.slice(0, -1);
14
- return new Response("", MakeStatus("Permanent Redirect", {
15
- headers: { location: url.toString() }
16
- }));
15
+ return redirect(url.toString(), { permanent: true });
17
16
  }
18
17
  export class HtmxRouterServer {
19
18
  vite;
20
19
  render;
21
20
  build;
22
- poweredBy;
21
+ headers;
23
22
  timers;
24
23
  #binding;
25
24
  constructor(config) {
26
25
  this.vite = config.viteDevServer;
27
- this.poweredBy = config.poweredBy === undefined ? true : config.poweredBy;
28
26
  this.timers = config.timers === undefined ? !!config.viteDevServer : config.timers;
29
27
  this.render = config.render;
30
28
  this.build = config.build;
29
+ this.headers = config.headers || new Headers();
30
+ if (config.poweredBy !== false && !this.headers.has("Powered-By"))
31
+ this.headers.set("X-Powered-By", "htmx-router");
32
+ if (!this.headers.has("Content-Type"))
33
+ this.headers.set("Content-Type", "text/html; charset=UTF-8");
31
34
  this.#binding = {
32
35
  pre: [UrlCleaner],
33
- post: []
36
+ post: [],
37
+ transform: []
34
38
  };
35
39
  if (this.vite) {
36
40
  const handler = connectToWeb(this.vite.middlewares);
@@ -51,24 +55,45 @@ export class HtmxRouterServer {
51
55
  }
52
56
  throw new Error(`Unknown binding type "${type}"`);
53
57
  }
58
+ /**
59
+ * Be careful with your transformers, there is no error unwrapping built in
60
+ * @param binding
61
+ * @returns
62
+ */
63
+ useTransform(binding) {
64
+ this.#binding.transform.push(binding);
65
+ return;
66
+ }
67
+ async transform(ctx, res) {
68
+ if (this.#binding.transform.length < 0)
69
+ return res;
70
+ ctx.timer.checkpoint("transform");
71
+ for (const process of this.#binding.transform) {
72
+ const next = process(ctx, res);
73
+ if (next instanceof Response)
74
+ res = next;
75
+ }
76
+ ctx.finalize(res);
77
+ return res;
78
+ }
54
79
  async resolve(request, resolve404 = true) {
55
80
  const url = new URL(request.url);
56
81
  const ctx = new GenericContext(request, url, this);
57
82
  { // pre-binding
58
83
  const res = await this.#applyBindings("pre", ctx);
59
84
  if (res)
60
- return ctx.finalize(res);
85
+ return await this.transform(ctx, res);
61
86
  }
62
- const tree = await this.#getTree();
63
87
  { // route
88
+ const tree = await this.#getTree();
64
89
  const res = await this.#resolveRoute(ctx, tree);
65
90
  if (res)
66
- return ctx.finalize(res);
91
+ return await this.transform(ctx, res);
67
92
  }
68
93
  { // post-binding
69
94
  const res = await this.#applyBindings("post", ctx);
70
95
  if (res)
71
- return ctx.finalize(res);
96
+ return await this.transform(ctx, res);
72
97
  }
73
98
  if (resolve404)
74
99
  return await this.error(ctx, undefined);
@@ -86,7 +111,7 @@ export class HtmxRouterServer {
86
111
  ctx = new GenericContext(ctx, new URL(ctx.url), this);
87
112
  const tree = await this.#getTree();
88
113
  const res = await tree.unwrap(ctx, e);
89
- return ctx.finalize(res);
114
+ return await this.transform(ctx, res);
90
115
  }
91
116
  /**
92
117
  * Create a closure for use with the classic express.js like servers
@@ -3,20 +3,18 @@ import { ParameterShaper } from '../util/parameters.js';
3
3
  import { RouteContext } from "../router.js";
4
4
  import { RequestTimer } from "../timer.js";
5
5
  import { Cookies } from '../cookies.js';
6
- type Rendered = Response | BodyInit;
7
6
  export declare class GenericContext {
8
- #private;
9
- request: Request;
10
- headers: Headers;
11
- cookie: Cookies;
12
- params: {
7
+ readonly scope: HtmxRouterServer;
8
+ readonly request: Request;
9
+ readonly headers: Headers;
10
+ readonly cookie: Cookies;
11
+ readonly params: {
13
12
  [key: string]: string;
14
13
  };
15
- timer: RequestTimer;
16
- url: URL;
14
+ readonly timer: RequestTimer;
15
+ readonly url: URL;
16
+ readonly render: HtmxRouterServer["render"];
17
17
  constructor(request: GenericContext["request"], url: GenericContext["url"], scope: HtmxRouterServer);
18
- render(res: JSX.Element, headers: Headers): Promise<Rendered> | Rendered;
19
- finalize(res: Response): Response;
18
+ finalize(res: Response): void;
20
19
  shape<T extends ParameterShaper>(shape: T, path: string): RouteContext<T>;
21
20
  }
22
- export {};
@@ -4,34 +4,25 @@ import { RouteContext } from "../router.js";
4
4
  import { RequestTimer } from "../timer.js";
5
5
  import { Cookies } from '../cookies.js';
6
6
  export class GenericContext {
7
+ scope;
7
8
  request;
8
9
  headers; // response headers
9
10
  cookie;
10
11
  params;
11
12
  timer;
12
13
  url;
13
- #server;
14
+ render;
14
15
  constructor(request, url, scope) {
15
- this.#server = scope;
16
16
  this.cookie = new Cookies(request.headers.get("cookie"));
17
- this.headers = new Headers();
17
+ this.headers = new Headers(scope.headers);
18
18
  this.request = request;
19
19
  this.params = {};
20
20
  this.url = url;
21
21
  this.timer = new RequestTimer(scope.timers);
22
- if (scope.poweredBy)
23
- this.headers.set("X-Powered-By", "htmx-router");
24
- this.headers.set("x-powered-by", "htmx-router");
25
- this.headers.set("content-type", "text/html");
26
- }
27
- render(res, headers) {
28
- this.timer.checkpoint("render");
29
- return this.#server.render(res, headers);
30
- }
31
- finalize(res) {
32
- this.timer.writeTo(res.headers);
33
- return res;
22
+ this.scope = scope;
23
+ this.render = scope.render;
34
24
  }
25
+ finalize(res) { this.timer.writeTo(res.headers); }
35
26
  shape(shape, path) {
36
27
  return new RouteContext(this, this.params, shape, path);
37
28
  }
@@ -19,7 +19,7 @@ export declare function refresh(init?: ResponseInit & {
19
19
  *
20
20
  * @param {Request} request - The incoming HTTP request object
21
21
  * @param {Headers} headers - The response headers object to modify
22
- * @param {string} etag - The current ETag value for the resource
22
+ * @param {string} etag - The current ETag value for the resource (do not quote)
23
23
  * @param {Object} [options] - Optional caching configuration
24
24
  * @param {number} [options.revalidate] - client must revalidate their etag at this interval in seconds
25
25
  * @param {boolean} [options.public] - cache visibility scope:
@@ -53,6 +53,8 @@ export declare function refresh(init?: ResponseInit & {
53
53
  * // This code only runs if client needs updated content
54
54
  * return new Response(generateContent(), { headers });
55
55
  * }
56
+ *
57
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/ETag | ETag - MDN Web Docs }
56
58
  */
57
59
  export declare function AssertETagStale(request: Request, headers: Headers, etag: string, options?: {
58
60
  revalidate?: number;
@@ -59,7 +59,7 @@ export function refresh(init) {
59
59
  *
60
60
  * @param {Request} request - The incoming HTTP request object
61
61
  * @param {Headers} headers - The response headers object to modify
62
- * @param {string} etag - The current ETag value for the resource
62
+ * @param {string} etag - The current ETag value for the resource (do not quote)
63
63
  * @param {Object} [options] - Optional caching configuration
64
64
  * @param {number} [options.revalidate] - client must revalidate their etag at this interval in seconds
65
65
  * @param {boolean} [options.public] - cache visibility scope:
@@ -93,6 +93,8 @@ export function refresh(init) {
93
93
  * // This code only runs if client needs updated content
94
94
  * return new Response(generateContent(), { headers });
95
95
  * }
96
+ *
97
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/ETag | ETag - MDN Web Docs }
96
98
  */
97
99
  export function AssertETagStale(request, headers, etag, options) {
98
100
  if (options) {
@@ -105,9 +107,9 @@ export function AssertETagStale(request, headers, etag, options) {
105
107
  headers.append("Cache-Control", `max-age=${options.revalidate}`);
106
108
  }
107
109
  headers.append("Cache-Control", "must-revalidate");
108
- headers.set("ETag", etag);
110
+ headers.set("ETag", `"${etag}"`);
109
111
  const client = request.headers.get("if-none-match");
110
- if (client === etag)
112
+ if (client !== etag)
111
113
  return;
112
114
  const res = new Response(null, {
113
115
  status: 304, statusText: "Not Modified",
@@ -189,17 +189,21 @@ class RouteLeaf {
189
189
  return null;
190
190
  if (jsx instanceof Response)
191
191
  return jsx;
192
- const res = await ctx.render(jsx, ctx.headers);
193
- if (res instanceof Response)
194
- return res;
192
+ ctx.timer.checkpoint("render");
193
+ const res = ctx.render(jsx);
195
194
  return html(res, { headers: ctx.headers });
196
195
  }
197
196
  async error(ctx, e) {
198
197
  if (!this.module.error)
199
198
  throw e;
200
- let jsx = await this.module.error(ctx.shape({}, this.path), e);
201
- const caught = jsx instanceof Response ? jsx
202
- : await ctx.render(jsx, ctx.headers);
199
+ const jsx = await this.module.error(ctx.shape({}, this.path), e);
200
+ let caught;
201
+ if (jsx instanceof Response)
202
+ caught = jsx;
203
+ else {
204
+ ctx.timer.checkpoint("render");
205
+ caught = ctx.render(jsx);
206
+ }
203
207
  if (caught instanceof Response) {
204
208
  caught.headers.set("X-Caught", "true");
205
209
  return caught;
@@ -45,4 +45,9 @@ export declare class RequestTimer {
45
45
  */
46
46
  checkpoint(name: string): void;
47
47
  writeTo(headers: Headers): void;
48
+ /**
49
+ * This action is non-reversible
50
+ * It will clear all timers for the request, and ensure they aren't included in the response
51
+ */
52
+ disable(): void;
48
53
  }
@@ -72,6 +72,11 @@ export class RequestTimer {
72
72
  const last = this.#checkpoints[limit];
73
73
  headers.set(`X-Time-${last.name}`, String(end - last.time));
74
74
  }
75
+ /**
76
+ * This action is non-reversible
77
+ * It will clear all timers for the request, and ensure they aren't included in the response
78
+ */
79
+ disable() { this.#checkpoints = null; }
75
80
  }
76
81
  class Checkpoint {
77
82
  time;
package/package.json CHANGED
@@ -1,19 +1,34 @@
1
1
  {
2
2
  "name": "htmx-router",
3
- "version": "2.0.5",
3
+ "version": "2.1.1",
4
4
  "description": "A lightweight SSR framework with server+client islands",
5
- "keywords": [
6
- "htmx",
7
- "router",
8
- "client islands",
9
- "ssr",
10
- "vite"
11
- ],
12
- "main": "./index.js",
5
+ "keywords": [ "htmx", "router", "client islands", "ssr", "vite" ],
13
6
  "type": "module",
14
7
  "scripts": {
15
8
  "build": "tsc"
16
9
  },
10
+ "main": "./index.js",
11
+ "exports": {
12
+ ".": "./dist/index.js",
13
+ "./cookies": "./dist/cookies.js",
14
+ "./css": "./dist/css.js",
15
+ "./defer": "./dist/defer.js",
16
+ "./endpoint": "./dist/endpoint.js",
17
+ "./event-source": "./dist/event-source.js",
18
+ "./navigate": "./dist/navigate.js",
19
+ "./response": "./dist/response.js",
20
+ "./router": "./dist/router.js",
21
+ "./server": "./dist/server.js",
22
+ "./shell": "./dist/shell.js",
23
+ "./status": "./dist/status.js",
24
+ "./timer": "./dist/timer.js",
25
+ "./vite": "./dist/vite/index.js",
26
+
27
+ "./util/parameters": "./dist/util/parameters.js",
28
+
29
+ "./internal/client": "./dist/internal/client.js",
30
+ "./internal/mount": "./dist/internal/mount.js"
31
+ },
17
32
  "bin": {
18
33
  "htmx-router": "cli/index.js"
19
34
  },
package/event-source.d.ts DELETED
@@ -1,31 +0,0 @@
1
- /**
2
- * Helper for Server-Sent-Events, with auto close on SIGTERM and SIGHUP messages
3
- * Includes a keep alive empty packet sent every 30sec (because Chrome implodes at 120sec, and can be unreliable at 60sec)
4
- */
5
- export declare class EventSource {
6
- #private;
7
- readonly response: Response;
8
- readonly createdAt: number;
9
- get updatedAt(): number;
10
- readonly withCredentials: boolean;
11
- readonly url: string;
12
- get readyState(): number;
13
- static CONNECTING: number;
14
- static OPEN: number;
15
- static CLOSED: number;
16
- constructor(request: Request, keepAlive?: number);
17
- isAborted(): boolean;
18
- private sendBytes;
19
- private sendText;
20
- private keepAlive;
21
- dispatch(type: string, data: string): boolean;
22
- close(unlink?: boolean): boolean;
23
- }
24
- export declare class EventSourceSet extends Set<EventSource> {
25
- /** Send update to all EventSources, auto closing failed dispatches */
26
- dispatch(type: string, data: string): void;
27
- /** Cull all closed connections */
28
- cull(): void;
29
- /** Close all connections */
30
- closeAll(): void;
31
- }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes