htmx-router 2.0.2 → 2.0.4
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/internal/request/server.d.ts +4 -0
- package/internal/request/server.js +11 -6
- package/internal/router.d.ts +7 -2
- package/internal/router.js +16 -3
- package/package.json +1 -1
- package/response.js +23 -18
- package/router.d.ts +2 -0
- package/router.js +2 -0
- package/timer.d.ts +48 -0
- package/timer.js +83 -0
|
@@ -5,6 +5,8 @@ export type Config = {
|
|
|
5
5
|
build: () => Promise<RouterModule> | Promise<RouterModule>;
|
|
6
6
|
render: GenericContext["render"];
|
|
7
7
|
viteDevServer: ViteDevServer | null;
|
|
8
|
+
poweredBy?: boolean;
|
|
9
|
+
timers?: boolean;
|
|
8
10
|
};
|
|
9
11
|
type ServerBindType = "pre" | "post";
|
|
10
12
|
type ServerBind = (ctx: GenericContext) => Promise<Response | null> | Response | null;
|
|
@@ -13,6 +15,8 @@ export declare class HtmxRouterServer {
|
|
|
13
15
|
readonly vite: Config["viteDevServer"];
|
|
14
16
|
readonly render: Config["render"];
|
|
15
17
|
readonly build: Config["build"];
|
|
18
|
+
readonly poweredBy: boolean;
|
|
19
|
+
readonly timers: boolean;
|
|
16
20
|
constructor(config: Config);
|
|
17
21
|
/**
|
|
18
22
|
* Add a middleware to resolve requests before/after the route tree attempts to resolve
|
|
@@ -19,9 +19,13 @@ export class HtmxRouterServer {
|
|
|
19
19
|
vite;
|
|
20
20
|
render;
|
|
21
21
|
build;
|
|
22
|
+
poweredBy;
|
|
23
|
+
timers;
|
|
22
24
|
#binding;
|
|
23
25
|
constructor(config) {
|
|
24
26
|
this.vite = config.viteDevServer;
|
|
27
|
+
this.poweredBy = config.poweredBy === undefined ? true : config.poweredBy;
|
|
28
|
+
this.timers = config.timers === undefined ? !!config.viteDevServer : config.timers;
|
|
25
29
|
this.render = config.render;
|
|
26
30
|
this.build = config.build;
|
|
27
31
|
this.#binding = {
|
|
@@ -49,22 +53,22 @@ export class HtmxRouterServer {
|
|
|
49
53
|
}
|
|
50
54
|
async resolve(request, resolve404 = true) {
|
|
51
55
|
const url = new URL(request.url);
|
|
52
|
-
const ctx = new GenericContext(request, url, this
|
|
56
|
+
const ctx = new GenericContext(request, url, this);
|
|
53
57
|
{ // pre-binding
|
|
54
58
|
const res = await this.#applyBindings("pre", ctx);
|
|
55
59
|
if (res)
|
|
56
|
-
return res;
|
|
60
|
+
return ctx.finalize(res);
|
|
57
61
|
}
|
|
58
62
|
const tree = await this.#getTree();
|
|
59
63
|
{ // route
|
|
60
64
|
const res = await this.#resolveRoute(ctx, tree);
|
|
61
65
|
if (res)
|
|
62
|
-
return res;
|
|
66
|
+
return ctx.finalize(res);
|
|
63
67
|
}
|
|
64
68
|
{ // post-binding
|
|
65
69
|
const res = await this.#applyBindings("post", ctx);
|
|
66
70
|
if (res)
|
|
67
|
-
return res;
|
|
71
|
+
return ctx.finalize(res);
|
|
68
72
|
}
|
|
69
73
|
if (resolve404)
|
|
70
74
|
return await this.error(ctx, undefined);
|
|
@@ -79,9 +83,10 @@ export class HtmxRouterServer {
|
|
|
79
83
|
if (e === undefined)
|
|
80
84
|
e = new Response("No Route", MakeStatus("Not Found"));
|
|
81
85
|
if (ctx instanceof Request)
|
|
82
|
-
ctx = new GenericContext(ctx, new URL(ctx.url), this
|
|
86
|
+
ctx = new GenericContext(ctx, new URL(ctx.url), this);
|
|
83
87
|
const tree = await this.#getTree();
|
|
84
|
-
|
|
88
|
+
const res = await tree.unwrap(ctx, e);
|
|
89
|
+
return ctx.finalize(res);
|
|
85
90
|
}
|
|
86
91
|
/**
|
|
87
92
|
* Create a closure for use with the classic express.js like servers
|
package/internal/router.d.ts
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
|
+
import type { HtmxRouterServer } from "./request/server.js";
|
|
1
2
|
import { ParameterShaper } from '../util/parameters.js';
|
|
2
3
|
import { RouteContext } from "../router.js";
|
|
4
|
+
import { RequestTimer } from "../timer.js";
|
|
3
5
|
import { Cookies } from '../cookies.js';
|
|
4
6
|
type Rendered = Response | BodyInit;
|
|
5
7
|
export declare class GenericContext {
|
|
8
|
+
#private;
|
|
6
9
|
request: Request;
|
|
7
10
|
headers: Headers;
|
|
8
11
|
cookie: Cookies;
|
|
9
12
|
params: {
|
|
10
13
|
[key: string]: string;
|
|
11
14
|
};
|
|
15
|
+
timer: RequestTimer;
|
|
12
16
|
url: URL;
|
|
13
|
-
|
|
14
|
-
|
|
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;
|
|
15
20
|
shape<T extends ParameterShaper>(shape: T, path: string): RouteContext<T>;
|
|
16
21
|
}
|
|
17
22
|
export {};
|
package/internal/router.js
CHANGED
|
@@ -1,24 +1,37 @@
|
|
|
1
1
|
import { ServerOnlyWarning } from "./util.js";
|
|
2
2
|
ServerOnlyWarning("internal/router");
|
|
3
3
|
import { RouteContext } from "../router.js";
|
|
4
|
+
import { RequestTimer } from "../timer.js";
|
|
4
5
|
import { Cookies } from '../cookies.js';
|
|
5
6
|
export class GenericContext {
|
|
6
7
|
request;
|
|
7
8
|
headers; // response headers
|
|
8
9
|
cookie;
|
|
9
10
|
params;
|
|
11
|
+
timer;
|
|
10
12
|
url;
|
|
11
|
-
|
|
12
|
-
constructor(request, url,
|
|
13
|
+
#server;
|
|
14
|
+
constructor(request, url, scope) {
|
|
15
|
+
this.#server = scope;
|
|
13
16
|
this.cookie = new Cookies(request.headers.get("cookie"));
|
|
14
17
|
this.headers = new Headers();
|
|
15
18
|
this.request = request;
|
|
16
19
|
this.params = {};
|
|
17
20
|
this.url = url;
|
|
18
|
-
this.
|
|
21
|
+
this.timer = new RequestTimer(scope.timers);
|
|
22
|
+
if (scope.poweredBy)
|
|
23
|
+
this.headers.set("X-Powered-By", "htmx-router");
|
|
19
24
|
this.headers.set("x-powered-by", "htmx-router");
|
|
20
25
|
this.headers.set("content-type", "text/html");
|
|
21
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;
|
|
34
|
+
}
|
|
22
35
|
shape(shape, path) {
|
|
23
36
|
return new RouteContext(this, this.params, shape, path);
|
|
24
37
|
}
|
package/package.json
CHANGED
package/response.js
CHANGED
|
@@ -1,55 +1,60 @@
|
|
|
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;
|
|
23
|
+
init = FillResponseInit(307, "Temporary Redirect", init);
|
|
32
24
|
const res = new Response("", init);
|
|
33
25
|
if (!init?.clientOnly)
|
|
34
26
|
res.headers.set("Location", url);
|
|
35
27
|
res.headers.set("HX-Location", url); // use hx-boost if applicable
|
|
28
|
+
res.headers.set("X-Caught", "true");
|
|
36
29
|
return res;
|
|
37
30
|
}
|
|
38
31
|
export function revalidate(init) {
|
|
39
|
-
init
|
|
40
|
-
init.statusText ||= "ok";
|
|
41
|
-
init.status ||= 200;
|
|
32
|
+
init = FillResponseInit(200, "Ok", init);
|
|
42
33
|
const res = new Response("", init);
|
|
43
34
|
res.headers.set("HX-Location", "");
|
|
35
|
+
res.headers.set("HX-Replace-Url", "");
|
|
36
|
+
res.headers.set("X-Caught", "true");
|
|
44
37
|
return res;
|
|
45
38
|
}
|
|
46
39
|
export function refresh(init) {
|
|
47
|
-
init
|
|
48
|
-
init.statusText ||= "ok";
|
|
49
|
-
init.status ||= 200;
|
|
40
|
+
init = FillResponseInit(200, "Ok", init);
|
|
50
41
|
const res = new Response("", init);
|
|
51
42
|
if (!init?.clientOnly)
|
|
52
43
|
res.headers.set("Refresh", "0"); // fallback
|
|
53
44
|
res.headers.set("HX-Refresh", "true");
|
|
45
|
+
res.headers.set("X-Caught", "true");
|
|
54
46
|
return res;
|
|
55
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* This is to fix issues with deno
|
|
50
|
+
* When you try and change the statusText on a Response object
|
|
51
|
+
*/
|
|
52
|
+
function FillResponseInit(status, statusText, init) {
|
|
53
|
+
if (init === undefined)
|
|
54
|
+
init = {};
|
|
55
|
+
if (init.statusText === undefined)
|
|
56
|
+
init.statusText = statusText;
|
|
57
|
+
if (init.status === undefined)
|
|
58
|
+
init.status = status;
|
|
59
|
+
return init;
|
|
60
|
+
}
|
package/router.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { GenericContext } from "./internal/router.js";
|
|
2
|
+
import type { RequestTimer } from "./timer.js";
|
|
2
3
|
import { Parameterized, ParameterPrelude, ParameterShaper } from './util/parameters.js';
|
|
3
4
|
import { RouteModule } from "./index.js";
|
|
4
5
|
import { Cookies } from './cookies.js';
|
|
@@ -13,6 +14,7 @@ export declare class RouteContext<T extends ParameterShaper = {}> {
|
|
|
13
14
|
readonly cookie: Cookies;
|
|
14
15
|
readonly params: Parameterized<T>;
|
|
15
16
|
readonly url: URL;
|
|
17
|
+
readonly timer: RequestTimer;
|
|
16
18
|
render: GenericContext["render"];
|
|
17
19
|
constructor(base: GenericContext | RouteContext, params: ParameterPrelude<T>, shape: T, path: string);
|
|
18
20
|
}
|
package/router.js
CHANGED
|
@@ -36,6 +36,7 @@ export class RouteContext {
|
|
|
36
36
|
cookie;
|
|
37
37
|
params;
|
|
38
38
|
url;
|
|
39
|
+
timer;
|
|
39
40
|
render;
|
|
40
41
|
constructor(base, params, shape, path) {
|
|
41
42
|
this.path = path;
|
|
@@ -43,6 +44,7 @@ export class RouteContext {
|
|
|
43
44
|
this.headers = base.headers;
|
|
44
45
|
this.request = base.request;
|
|
45
46
|
this.render = base.render;
|
|
47
|
+
this.timer = base.timer;
|
|
46
48
|
this.url = base.url;
|
|
47
49
|
this.params = {};
|
|
48
50
|
for (const key in shape) {
|
package/timer.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tracks timing checkpoints during request processing and writes performance
|
|
3
|
+
* metrics to HTTP response headers.
|
|
4
|
+
*
|
|
5
|
+
* **HTTP Headers Generated:**
|
|
6
|
+
* - `X-Time-Total`: Total request processing time in milliseconds
|
|
7
|
+
* - `X-Time-{name}`: Duration for each checkpoint in milliseconds
|
|
8
|
+
*
|
|
9
|
+
* **Default Checkpoints:**
|
|
10
|
+
* - `route`: Automatically created when timer is enabled (marks start of request)
|
|
11
|
+
* - `render`: Automatically triggered when JSX gets rendered to a string
|
|
12
|
+
*/
|
|
13
|
+
export declare class RequestTimer {
|
|
14
|
+
#private;
|
|
15
|
+
constructor(enabled: boolean);
|
|
16
|
+
/**
|
|
17
|
+
* Creates a timing checkpoint with the given name.
|
|
18
|
+
*
|
|
19
|
+
* Each checkpoint measures the time elapsed since the previous checkpoint.
|
|
20
|
+
* The resulting duration is written to the `X-Time-{name}` HTTP header.
|
|
21
|
+
*
|
|
22
|
+
* **Common Usage Pattern:**
|
|
23
|
+
* ```ts
|
|
24
|
+
* timer.checkpoint("auth"); // -> X-Time-auth: time from auth to fetch
|
|
25
|
+
* timer.checkpoint("fetch"); // -> X-Time-auth: time from fetch to transform
|
|
26
|
+
* timer.checkpoint("transform"); // -> X-Time-fetch: time from transform to render
|
|
27
|
+
* // render checkpoint added automatically during JSX rendering
|
|
28
|
+
* // // -> X-Time-render: time from render to response
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* @param name - The name of the checkpoint (used in the HTTP header)
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* export async function loader({ timer }: RouteContext) {
|
|
36
|
+
* timer.checkpoint("auth");
|
|
37
|
+
* await authenticate();
|
|
38
|
+
*
|
|
39
|
+
* timer.checkpoint("fetch");
|
|
40
|
+
* const data = await fetchData();
|
|
41
|
+
*
|
|
42
|
+
* return json(data);
|
|
43
|
+
* }
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
checkpoint(name: string): void;
|
|
47
|
+
writeTo(headers: Headers): void;
|
|
48
|
+
}
|
package/timer.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tracks timing checkpoints during request processing and writes performance
|
|
3
|
+
* metrics to HTTP response headers.
|
|
4
|
+
*
|
|
5
|
+
* **HTTP Headers Generated:**
|
|
6
|
+
* - `X-Time-Total`: Total request processing time in milliseconds
|
|
7
|
+
* - `X-Time-{name}`: Duration for each checkpoint in milliseconds
|
|
8
|
+
*
|
|
9
|
+
* **Default Checkpoints:**
|
|
10
|
+
* - `route`: Automatically created when timer is enabled (marks start of request)
|
|
11
|
+
* - `render`: Automatically triggered when JSX gets rendered to a string
|
|
12
|
+
*/
|
|
13
|
+
export class RequestTimer {
|
|
14
|
+
#checkpoints;
|
|
15
|
+
constructor(enabled) {
|
|
16
|
+
if (!enabled) {
|
|
17
|
+
this.#checkpoints = null;
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
this.#checkpoints = [];
|
|
21
|
+
this.checkpoint("route");
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Creates a timing checkpoint with the given name.
|
|
25
|
+
*
|
|
26
|
+
* Each checkpoint measures the time elapsed since the previous checkpoint.
|
|
27
|
+
* The resulting duration is written to the `X-Time-{name}` HTTP header.
|
|
28
|
+
*
|
|
29
|
+
* **Common Usage Pattern:**
|
|
30
|
+
* ```ts
|
|
31
|
+
* timer.checkpoint("auth"); // -> X-Time-auth: time from auth to fetch
|
|
32
|
+
* timer.checkpoint("fetch"); // -> X-Time-auth: time from fetch to transform
|
|
33
|
+
* timer.checkpoint("transform"); // -> X-Time-fetch: time from transform to render
|
|
34
|
+
* // render checkpoint added automatically during JSX rendering
|
|
35
|
+
* // // -> X-Time-render: time from render to response
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* @param name - The name of the checkpoint (used in the HTTP header)
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```ts
|
|
42
|
+
* export async function loader({ timer }: RouteContext) {
|
|
43
|
+
* timer.checkpoint("auth");
|
|
44
|
+
* await authenticate();
|
|
45
|
+
*
|
|
46
|
+
* timer.checkpoint("fetch");
|
|
47
|
+
* const data = await fetchData();
|
|
48
|
+
*
|
|
49
|
+
* return json(data);
|
|
50
|
+
* }
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
checkpoint(name) {
|
|
54
|
+
if (this.#checkpoints === null)
|
|
55
|
+
return;
|
|
56
|
+
this.#checkpoints.push(new Checkpoint(name));
|
|
57
|
+
}
|
|
58
|
+
writeTo(headers) {
|
|
59
|
+
if (this.#checkpoints === null)
|
|
60
|
+
return;
|
|
61
|
+
if (this.#checkpoints.length < 1)
|
|
62
|
+
return;
|
|
63
|
+
const end = Date.now();
|
|
64
|
+
const first = this.#checkpoints[0];
|
|
65
|
+
headers.set(`X-Time-Total`, String(end - first.time));
|
|
66
|
+
const limit = this.#checkpoints.length - 1;
|
|
67
|
+
for (let i = 0; i < limit; i++) {
|
|
68
|
+
const c = this.#checkpoints[i];
|
|
69
|
+
const n = this.#checkpoints[i + 1];
|
|
70
|
+
headers.set(`X-Time-${c.name}`, String(n.time - c.time));
|
|
71
|
+
}
|
|
72
|
+
const last = this.#checkpoints[limit];
|
|
73
|
+
headers.set(`X-Time-${last.name}`, String(end - last.time));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
class Checkpoint {
|
|
77
|
+
time;
|
|
78
|
+
name;
|
|
79
|
+
constructor(name) {
|
|
80
|
+
this.time = Date.now();
|
|
81
|
+
this.name = name;
|
|
82
|
+
}
|
|
83
|
+
}
|