htmx-router 2.0.6 → 2.1.2
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/dist/event-source.d.ts +61 -0
- package/{event-source.js → dist/event-source.js} +103 -27
- package/{internal → dist/internal}/request/server.d.ts +11 -2
- package/{internal → dist/internal}/request/server.js +36 -11
- package/{internal → dist/internal}/router.d.ts +9 -11
- package/{internal → dist/internal}/router.js +6 -15
- package/{response.d.ts → dist/response.d.ts} +3 -1
- package/{response.js → dist/response.js} +4 -2
- package/{router.js → dist/router.js} +7 -6
- package/{timer.d.ts → dist/timer.d.ts} +5 -0
- package/{timer.js → dist/timer.js} +5 -0
- package/package.json +25 -10
- package/event-source.d.ts +0 -31
- /package/{cli → dist/cli}/config.d.ts +0 -0
- /package/{cli → dist/cli}/config.js +0 -0
- /package/{cli → dist/cli}/index.d.ts +0 -0
- /package/{cli → dist/cli}/index.js +0 -0
- /package/{cookies.d.ts → dist/cookies.d.ts} +0 -0
- /package/{cookies.js → dist/cookies.js} +0 -0
- /package/{css.d.ts → dist/css.d.ts} +0 -0
- /package/{css.js → dist/css.js} +0 -0
- /package/{defer.d.ts → dist/defer.d.ts} +0 -0
- /package/{defer.js → dist/defer.js} +0 -0
- /package/{endpoint.d.ts → dist/endpoint.d.ts} +0 -0
- /package/{endpoint.js → dist/endpoint.js} +0 -0
- /package/{index.d.ts → dist/index.d.ts} +0 -0
- /package/{index.js → dist/index.js} +0 -0
- /package/{internal → dist/internal}/client.d.ts +0 -0
- /package/{internal → dist/internal}/client.js +0 -0
- /package/{internal → dist/internal}/compile/manifest.d.ts +0 -0
- /package/{internal → dist/internal}/compile/manifest.js +0 -0
- /package/{internal → dist/internal}/component/defer.d.ts +0 -0
- /package/{internal → dist/internal}/component/defer.js +0 -0
- /package/{internal → dist/internal}/component/head.d.ts +0 -0
- /package/{internal → dist/internal}/component/head.js +0 -0
- /package/{internal → dist/internal}/component/index.d.ts +0 -0
- /package/{internal → dist/internal}/component/index.js +0 -0
- /package/{internal → dist/internal}/component/scripts.d.ts +0 -0
- /package/{internal → dist/internal}/component/scripts.js +0 -0
- /package/{internal → dist/internal}/mount.d.ts +0 -0
- /package/{internal → dist/internal}/mount.js +0 -0
- /package/{internal → dist/internal}/request/compatibility/node.d.ts +0 -0
- /package/{internal → dist/internal}/request/compatibility/node.js +0 -0
- /package/{internal → dist/internal}/request/compatibility/vite/connectToWeb.d.ts +0 -0
- /package/{internal → dist/internal}/request/compatibility/vite/connectToWeb.js +0 -0
- /package/{internal → dist/internal}/request/compatibility/vite/createServerResponse.d.ts +0 -0
- /package/{internal → dist/internal}/request/compatibility/vite/createServerResponse.js +0 -0
- /package/{internal → dist/internal}/request/compatibility/vite/header-utils.d.ts +0 -0
- /package/{internal → dist/internal}/request/compatibility/vite/header-utils.js +0 -0
- /package/{internal → dist/internal}/request/index.d.ts +0 -0
- /package/{internal → dist/internal}/request/index.js +0 -0
- /package/{internal → dist/internal}/util.d.ts +0 -0
- /package/{internal → dist/internal}/util.js +0 -0
- /package/{navigate.d.ts → dist/navigate.d.ts} +0 -0
- /package/{navigate.js → dist/navigate.js} +0 -0
- /package/{router.d.ts → dist/router.d.ts} +0 -0
- /package/{server.d.ts → dist/server.d.ts} +0 -0
- /package/{server.js → dist/server.js} +0 -0
- /package/{shell.d.ts → dist/shell.d.ts} +0 -0
- /package/{shell.js → dist/shell.js} +0 -0
- /package/{status.d.ts → dist/status.d.ts} +0 -0
- /package/{status.js → dist/status.js} +0 -0
- /package/{util → dist/util}/parameters.d.ts +0 -0
- /package/{util → dist/util}/parameters.js +0 -0
- /package/{util → dist/util}/path-builder.d.ts +0 -0
- /package/{util → dist/util}/path-builder.js +0 -0
- /package/{util → dist/util}/route.d.ts +0 -0
- /package/{util → dist/util}/route.js +0 -0
- /package/{vite → dist/vite}/bundle-splitter.d.ts +0 -0
- /package/{vite → dist/vite}/bundle-splitter.js +0 -0
- /package/{vite → dist/vite}/client-island.d.ts +0 -0
- /package/{vite → dist/vite}/client-island.js +0 -0
- /package/{vite → dist/vite}/index.d.ts +0 -0
- /package/{vite → dist/vite}/index.js +0 -0
- /package/{vite → dist/vite}/router.d.ts +0 -0
- /package/{vite → dist/vite}/router.js +0 -0
|
@@ -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,
|
|
38
|
+
constructor(request, render) {
|
|
38
39
|
this.#controller = null;
|
|
39
|
-
this.#state =
|
|
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
|
|
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
|
-
|
|
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 ===
|
|
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
|
|
79
|
+
#sendText(chunk, simulated) {
|
|
80
|
+
return this.#sendBytes(encoder.encode(chunk), simulated);
|
|
80
81
|
}
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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
|
-
|
|
99
|
-
clearInterval(this.#timer);
|
|
100
|
-
if (unlink)
|
|
101
|
-
register.delete(this);
|
|
111
|
+
keepAlive.delete(this);
|
|
102
112
|
// was already closed
|
|
103
|
-
if (this.#state ===
|
|
113
|
+
if (this.#state === _a.CLOSED)
|
|
104
114
|
return false;
|
|
105
115
|
// Mark closed
|
|
106
|
-
this.#state =
|
|
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
|
|
139
|
-
|
|
140
|
-
|
|
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',
|
|
144
|
-
process.on('
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
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 < 1)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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,7 +107,7 @@ 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
112
|
if (client !== etag)
|
|
111
113
|
return;
|
|
@@ -189,17 +189,18 @@ class RouteLeaf {
|
|
|
189
189
|
return null;
|
|
190
190
|
if (jsx instanceof Response)
|
|
191
191
|
return jsx;
|
|
192
|
-
const res =
|
|
193
|
-
if (res instanceof Response)
|
|
194
|
-
return res;
|
|
192
|
+
const res = ctx.render(jsx);
|
|
195
193
|
return html(res, { headers: ctx.headers });
|
|
196
194
|
}
|
|
197
195
|
async error(ctx, e) {
|
|
198
196
|
if (!this.module.error)
|
|
199
197
|
throw e;
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
198
|
+
const jsx = await this.module.error(ctx.shape({}, this.path), e);
|
|
199
|
+
let caught;
|
|
200
|
+
if (jsx instanceof Response)
|
|
201
|
+
caught = jsx;
|
|
202
|
+
else
|
|
203
|
+
caught = ctx.render(jsx);
|
|
203
204
|
if (caught instanceof Response) {
|
|
204
205
|
caught.headers.set("X-Caught", "true");
|
|
205
206
|
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,21 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "htmx-router",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.2",
|
|
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": "./dist/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
|
-
"htmx-router": "cli/index.js"
|
|
33
|
+
"htmx-router": "dist/cli/index.js"
|
|
19
34
|
},
|
|
20
35
|
"repository": {
|
|
21
36
|
"type": "git",
|
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
|
/package/{css.js → dist/css.js}
RENAMED
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|