htmx-router 2.1.6 → 2.2.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.
- package/dist/event-source.d.ts +11 -1
- package/dist/event-source.js +51 -25
- package/dist/event.d.ts +31 -0
- package/dist/event.js +53 -0
- package/dist/internal/component/defer.js +2 -0
- package/dist/internal/request/server.js +16 -7
- package/dist/router.d.ts +11 -11
- package/dist/router.js +87 -114
- package/dist/util/singleton.d.ts +1 -0
- package/dist/util/singleton.js +8 -0
- package/package.json +2 -1
package/dist/event-source.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ type Render = (jsx: JSX.Element) => string;
|
|
|
5
5
|
*/
|
|
6
6
|
export declare class EventSource<JsxEnabled extends boolean = false> {
|
|
7
7
|
#private;
|
|
8
|
+
readonly _signal: AbortSignal;
|
|
8
9
|
readonly response: Response;
|
|
9
10
|
readonly createdAt: number;
|
|
10
11
|
get updatedAt(): number;
|
|
@@ -20,11 +21,15 @@ export declare class EventSource<JsxEnabled extends boolean = false> {
|
|
|
20
21
|
* For internal use only
|
|
21
22
|
* @deprecated
|
|
22
23
|
*/
|
|
23
|
-
|
|
24
|
+
pulse(): boolean;
|
|
24
25
|
dispatch(type: string, data: JsxEnabled extends true ? (JSX.Element | string) : string): boolean;
|
|
25
26
|
close(): boolean;
|
|
26
27
|
}
|
|
27
28
|
export declare class EventSourceSet<JsxEnabled extends boolean = false> extends Set<EventSource<JsxEnabled>> {
|
|
29
|
+
private onAbort;
|
|
30
|
+
constructor();
|
|
31
|
+
add(stream: EventSource<JsxEnabled>): this;
|
|
32
|
+
delete(stream: EventSource<JsxEnabled>): boolean;
|
|
28
33
|
/**
|
|
29
34
|
* Send update to all EventSources, auto closing failed dispatches
|
|
30
35
|
* @returns number of successful sends
|
|
@@ -35,6 +40,11 @@ export declare class EventSourceSet<JsxEnabled extends boolean = false> extends
|
|
|
35
40
|
* @returns number of connections closed
|
|
36
41
|
*/
|
|
37
42
|
cull(): number;
|
|
43
|
+
/**
|
|
44
|
+
* INTERNAL: Send keep-alive to all EventSources
|
|
45
|
+
* @deprecated
|
|
46
|
+
*/
|
|
47
|
+
pulse(): number;
|
|
38
48
|
/**
|
|
39
49
|
* Close all connections
|
|
40
50
|
* @returns number of connections closed
|
package/dist/event-source.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
var _a;
|
|
2
2
|
import { ServerOnlyWarning } from "./internal/util.js";
|
|
3
|
+
import { MakeStatus } from './status.js';
|
|
4
|
+
import { Lifecycle } from "./event.js";
|
|
3
5
|
ServerOnlyWarning("event-source");
|
|
4
6
|
// global for easy reuse
|
|
5
7
|
const encoder = new TextEncoder();
|
|
@@ -17,8 +19,8 @@ const headers = {
|
|
|
17
19
|
* Includes a keep alive empty packet sent every 30sec (because Chrome implodes at 120sec, and can be unreliable at 60sec)
|
|
18
20
|
*/
|
|
19
21
|
export class EventSource {
|
|
22
|
+
_signal;
|
|
20
23
|
#controller;
|
|
21
|
-
#signal;
|
|
22
24
|
#state;
|
|
23
25
|
#render;
|
|
24
26
|
response;
|
|
@@ -38,21 +40,26 @@ export class EventSource {
|
|
|
38
40
|
constructor(request, render) {
|
|
39
41
|
this.#controller = null;
|
|
40
42
|
this.#state = _a.CONNECTING;
|
|
43
|
+
this._signal = request.signal;
|
|
41
44
|
this.#render = render;
|
|
42
45
|
this.withCredentials = request.mode === "cors";
|
|
43
46
|
this.url = request.url;
|
|
44
47
|
this.createdAt = Date.now();
|
|
45
48
|
this.#updatedAt = 0;
|
|
49
|
+
if (Lifecycle.isShuttingDown()) {
|
|
50
|
+
this.response = new Response('Server is shutting down', MakeStatus('Service Unavailable'));
|
|
51
|
+
this.#state = _a.CLOSED;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
46
54
|
// immediate prepare for abortion
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
this.#signal.addEventListener('abort', cancel);
|
|
55
|
+
const cancel = () => { this.close(); this._signal.removeEventListener("abort", cancel); };
|
|
56
|
+
this._signal.addEventListener('abort', cancel);
|
|
50
57
|
const start = (c) => { this.#controller = c; this.#state = _a.OPEN; };
|
|
51
58
|
const stream = new ReadableStream({ start, cancel }, { highWaterMark: 0 });
|
|
52
59
|
this.response = new Response(stream, { headers });
|
|
53
60
|
keepAlive.add(this);
|
|
54
61
|
}
|
|
55
|
-
isAborted() { return this
|
|
62
|
+
isAborted() { return this._signal.aborted; }
|
|
56
63
|
#sendBytes(chunk, active) {
|
|
57
64
|
if (this.#state === _a.CLOSED) {
|
|
58
65
|
const err = new Error(`Warn: Attempted to send data on closed stream for: ${this.url}`);
|
|
@@ -83,7 +90,7 @@ export class EventSource {
|
|
|
83
90
|
* For internal use only
|
|
84
91
|
* @deprecated
|
|
85
92
|
*/
|
|
86
|
-
|
|
93
|
+
pulse() {
|
|
87
94
|
return this.#sendText("\n\n", false);
|
|
88
95
|
}
|
|
89
96
|
dispatch(type, data) {
|
|
@@ -119,6 +126,19 @@ export class EventSource {
|
|
|
119
126
|
}
|
|
120
127
|
_a = EventSource;
|
|
121
128
|
export class EventSourceSet extends Set {
|
|
129
|
+
onAbort;
|
|
130
|
+
constructor() {
|
|
131
|
+
super();
|
|
132
|
+
this.onAbort = () => this.cull();
|
|
133
|
+
}
|
|
134
|
+
add(stream) {
|
|
135
|
+
stream._signal.addEventListener('abort', this.onAbort);
|
|
136
|
+
return super.add(stream);
|
|
137
|
+
}
|
|
138
|
+
delete(stream) {
|
|
139
|
+
stream._signal.removeEventListener('abort', this.onAbort);
|
|
140
|
+
return super.delete(stream);
|
|
141
|
+
}
|
|
122
142
|
/**
|
|
123
143
|
* Send update to all EventSources, auto closing failed dispatches
|
|
124
144
|
* @returns number of successful sends
|
|
@@ -126,8 +146,8 @@ export class EventSourceSet extends Set {
|
|
|
126
146
|
dispatch(type, data) {
|
|
127
147
|
let count = 0;
|
|
128
148
|
for (const stream of this) {
|
|
129
|
-
if (stream.readyState
|
|
130
|
-
continue; // skip
|
|
149
|
+
if (stream.readyState !== EventSource.OPEN)
|
|
150
|
+
continue; // skip closed
|
|
131
151
|
const success = stream.dispatch(type, data);
|
|
132
152
|
if (success)
|
|
133
153
|
count++;
|
|
@@ -143,12 +163,27 @@ export class EventSourceSet extends Set {
|
|
|
143
163
|
cull() {
|
|
144
164
|
const count = this.size;
|
|
145
165
|
for (const stream of this) {
|
|
146
|
-
if (stream.readyState !==
|
|
166
|
+
if (stream.readyState !== EventSource.CLOSED)
|
|
147
167
|
continue;
|
|
148
168
|
this.delete(stream);
|
|
149
169
|
}
|
|
150
170
|
return count;
|
|
151
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* INTERNAL: Send keep-alive to all EventSources
|
|
174
|
+
* @deprecated
|
|
175
|
+
*/
|
|
176
|
+
pulse() {
|
|
177
|
+
let count = 0;
|
|
178
|
+
for (const stream of this) {
|
|
179
|
+
if (stream.readyState !== EventSource.OPEN)
|
|
180
|
+
continue; // skip closed
|
|
181
|
+
const success = stream.pulse();
|
|
182
|
+
if (success)
|
|
183
|
+
count++;
|
|
184
|
+
}
|
|
185
|
+
return count;
|
|
186
|
+
}
|
|
152
187
|
/**
|
|
153
188
|
* Close all connections
|
|
154
189
|
* @returns number of connections closed
|
|
@@ -171,8 +206,8 @@ export class SharedEventSource {
|
|
|
171
206
|
#cache;
|
|
172
207
|
#rules;
|
|
173
208
|
constructor(props) {
|
|
174
|
-
this.#pool = new EventSourceSet();
|
|
175
209
|
this.#render = props?.render || undefined;
|
|
210
|
+
this.#pool = new EventSourceSet();
|
|
176
211
|
this.#cache = {};
|
|
177
212
|
this.#rules = {};
|
|
178
213
|
}
|
|
@@ -221,18 +256,9 @@ export class SharedEventSource {
|
|
|
221
256
|
}
|
|
222
257
|
// Auto close all SSE streams when shutdown requested
|
|
223
258
|
// Without this graceful shutdowns will hang indefinitely
|
|
224
|
-
const keepAlive = new
|
|
225
|
-
const interval = setInterval(() => {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}
|
|
231
|
-
e._keepAlive();
|
|
232
|
-
}
|
|
233
|
-
}, 10_000);
|
|
234
|
-
function Shutdown() { clearInterval(interval); }
|
|
235
|
-
if (process) {
|
|
236
|
-
process.on('SIGTERM', Shutdown);
|
|
237
|
-
process.on('SIGHUP', Shutdown);
|
|
238
|
-
}
|
|
259
|
+
const keepAlive = new EventSourceSet();
|
|
260
|
+
const interval = setInterval(() => { keepAlive.pulse(); }, 10_000);
|
|
261
|
+
Lifecycle.addEventListener('shutdown', () => {
|
|
262
|
+
clearInterval(interval);
|
|
263
|
+
keepAlive.closeAll();
|
|
264
|
+
});
|
package/dist/event.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
declare global {
|
|
2
|
+
const Deno: {
|
|
3
|
+
build: {
|
|
4
|
+
os: 'windows' | 'linux' | 'darwin' | string;
|
|
5
|
+
};
|
|
6
|
+
addSignalListener: (signal: string, handler: () => void) => void;
|
|
7
|
+
} | undefined;
|
|
8
|
+
}
|
|
9
|
+
export declare class ShutdownEvent extends CustomEvent<{
|
|
10
|
+
signal: string;
|
|
11
|
+
}> {
|
|
12
|
+
constructor(signal: string);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Controller for handling graceful shutdowns across Windows, Linux, and Browsers.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* Lifecycle.addEventListener('shutdown', (e) => {
|
|
20
|
+
* console.log('Stopping because of:', e.detail.signal);
|
|
21
|
+
* server.close();
|
|
22
|
+
* });
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
declare class LifecycleController extends EventTarget {
|
|
26
|
+
#private;
|
|
27
|
+
constructor();
|
|
28
|
+
isShuttingDown(): boolean;
|
|
29
|
+
}
|
|
30
|
+
export declare const Lifecycle: LifecycleController;
|
|
31
|
+
export {};
|
package/dist/event.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Singleton } from './util/singleton';
|
|
2
|
+
export class ShutdownEvent extends CustomEvent {
|
|
3
|
+
constructor(signal) {
|
|
4
|
+
super('shutdown', { detail: { signal } });
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Controller for handling graceful shutdowns across Windows, Linux, and Browsers.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* Lifecycle.addEventListener('shutdown', (e) => {
|
|
13
|
+
* console.log('Stopping because of:', e.detail.signal);
|
|
14
|
+
* server.close();
|
|
15
|
+
* });
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
class LifecycleController extends EventTarget {
|
|
19
|
+
#shuttingDown = false;
|
|
20
|
+
constructor() {
|
|
21
|
+
super();
|
|
22
|
+
this.#bindEvents();
|
|
23
|
+
}
|
|
24
|
+
isShuttingDown() { return this.#shuttingDown; }
|
|
25
|
+
// Bind to whatever environment we are running in
|
|
26
|
+
#bindEvents() {
|
|
27
|
+
const trigger = (sig) => this.#trigger(sig);
|
|
28
|
+
if (globalThis.window && globalThis.window.addEventListener) {
|
|
29
|
+
// BROWSER: Tab closing / Refreshing
|
|
30
|
+
globalThis.window.addEventListener('beforeunload', () => trigger('BEFOREUNLOAD'));
|
|
31
|
+
}
|
|
32
|
+
if (process) {
|
|
33
|
+
// process.on('SIGINT', () => trigger('SIGINT'));
|
|
34
|
+
process.on('SIGTERM', () => trigger('SIGTERM'));
|
|
35
|
+
process.on('SIGHUP', () => trigger('SIGHUP'));
|
|
36
|
+
}
|
|
37
|
+
if (Deno) {
|
|
38
|
+
if (Deno.build.os !== "windows") {
|
|
39
|
+
Deno.addSignalListener('SIGTERM', () => trigger('SIGTERM'));
|
|
40
|
+
}
|
|
41
|
+
Deno.addSignalListener("SIGHUP", () => trigger('SIGHUP'));
|
|
42
|
+
// Deno.addSignalListener("SIGINT", () => trigger('SIGINT'));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
#trigger(signal) {
|
|
46
|
+
if (this.#shuttingDown)
|
|
47
|
+
return;
|
|
48
|
+
this.#shuttingDown = true;
|
|
49
|
+
console.log(`\n🛑 Shutdown Signal received: ${signal}`);
|
|
50
|
+
this.dispatchEvent(new ShutdownEvent(signal));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export const Lifecycle = Singleton('htmx-router-lifecycle', () => new LifecycleController());
|
|
@@ -11,6 +11,8 @@ export function Defer<T extends ParameterShaper>(props: {
|
|
|
11
11
|
hx-get={Deferral(props.loader, props.params)}
|
|
12
12
|
hx-trigger="load"
|
|
13
13
|
hx-swap="outerHTML transition:true"
|
|
14
|
+
hx-replace-url="false"
|
|
15
|
+
hx-push-url="false"
|
|
14
16
|
style={{ display: "contents" }}
|
|
15
17
|
>{props.children ? props.children : ""}</div>
|
|
16
18
|
}`;
|
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
import { ServerOnlyWarning } from "../util.js";
|
|
2
2
|
ServerOnlyWarning("request/server");
|
|
3
3
|
import { connectToWeb } from "./compatibility/vite/connectToWeb.js";
|
|
4
|
+
import { RouteResolver } from "../../router.js";
|
|
4
5
|
import { GenericContext } from "../router.js";
|
|
5
6
|
import { NodeAdaptor } from "./compatibility/node.js";
|
|
6
7
|
import { MakeStatus } from "../../status.js";
|
|
7
8
|
import { redirect } from "../../response.js";
|
|
8
9
|
function UrlCleaner({ url }) {
|
|
9
|
-
|
|
10
|
+
let i = url.pathname.lastIndexOf("/");
|
|
10
11
|
if (i === 0)
|
|
11
12
|
return null;
|
|
12
13
|
if (i !== url.pathname.length - 1)
|
|
13
14
|
return null;
|
|
14
|
-
|
|
15
|
+
i--;
|
|
16
|
+
while (url.pathname[i] === '/' && i > 0)
|
|
17
|
+
i--;
|
|
18
|
+
if (i === 0)
|
|
19
|
+
url.pathname = '/';
|
|
20
|
+
else
|
|
21
|
+
url.pathname = url.pathname.slice(0, i);
|
|
15
22
|
return redirect(url.toString(), { permanent: true });
|
|
16
23
|
}
|
|
17
24
|
export class HtmxRouterServer {
|
|
@@ -110,7 +117,8 @@ export class HtmxRouterServer {
|
|
|
110
117
|
if (ctx instanceof Request)
|
|
111
118
|
ctx = new GenericContext(ctx, new URL(ctx.url), this);
|
|
112
119
|
const tree = await this.#getTree();
|
|
113
|
-
const
|
|
120
|
+
const chain = new RouteResolver(ctx, [], tree);
|
|
121
|
+
const res = await chain.unwind(e, 0);
|
|
114
122
|
return await this.transform(ctx, res);
|
|
115
123
|
}
|
|
116
124
|
/**
|
|
@@ -125,10 +133,11 @@ export class HtmxRouterServer {
|
|
|
125
133
|
}
|
|
126
134
|
async #resolveRoute(ctx, tree) {
|
|
127
135
|
let response;
|
|
136
|
+
const x = ctx.url.pathname.slice(1);
|
|
137
|
+
const fragments = x === "" ? [] : x.split("/");
|
|
138
|
+
const chain = new RouteResolver(ctx, fragments, tree);
|
|
128
139
|
try {
|
|
129
|
-
const
|
|
130
|
-
const fragments = x === "" ? [] : x.split("/");
|
|
131
|
-
const res = await tree.resolve(fragments, ctx);
|
|
140
|
+
const res = await chain.resolve();
|
|
132
141
|
if (res === null)
|
|
133
142
|
return null;
|
|
134
143
|
response = res;
|
|
@@ -137,7 +146,7 @@ export class HtmxRouterServer {
|
|
|
137
146
|
if (e instanceof Error)
|
|
138
147
|
this.vite?.ssrFixStacktrace(e);
|
|
139
148
|
console.error(e);
|
|
140
|
-
response = await
|
|
149
|
+
response = await chain.unwind(e, 0);
|
|
141
150
|
}
|
|
142
151
|
// context merge headers if divergent
|
|
143
152
|
if (response.headers !== ctx.headers) {
|
package/dist/router.d.ts
CHANGED
|
@@ -18,6 +18,15 @@ export declare class RouteContext<T extends ParameterShaper = {}> {
|
|
|
18
18
|
render: GenericContext["render"];
|
|
19
19
|
constructor(base: GenericContext | RouteContext, params: ParameterPrelude<T>, shape: T, path: string);
|
|
20
20
|
}
|
|
21
|
+
export declare class RouteResolver {
|
|
22
|
+
private stack;
|
|
23
|
+
private slugs;
|
|
24
|
+
readonly ctx: GenericContext;
|
|
25
|
+
constructor(ctx: GenericContext, fragments: string[], tree: RouteTree);
|
|
26
|
+
push(leaf: RouteLeaf, slug?: string): void;
|
|
27
|
+
resolve(): Promise<Response | null>;
|
|
28
|
+
unwind(e: unknown, offset: number): Promise<Response>;
|
|
29
|
+
}
|
|
21
30
|
export declare class RouteTree {
|
|
22
31
|
private nested;
|
|
23
32
|
private index;
|
|
@@ -26,20 +35,11 @@ export declare class RouteTree {
|
|
|
26
35
|
private wildCard;
|
|
27
36
|
constructor();
|
|
28
37
|
ingest(node: RouteLeaf, path?: string[]): void;
|
|
29
|
-
|
|
30
|
-
private _resolve;
|
|
31
|
-
private resolveIndex;
|
|
32
|
-
private resolveNext;
|
|
33
|
-
private resolveWild;
|
|
34
|
-
private resolveSlug;
|
|
35
|
-
unwrap(ctx: GenericContext, res: unknown): Promise<Response>;
|
|
38
|
+
_applyChain(out: RouteResolver, fragments: string[], offset?: number): void;
|
|
36
39
|
}
|
|
37
40
|
declare class RouteLeaf {
|
|
38
|
-
|
|
41
|
+
readonly module: RouteModule<any>;
|
|
39
42
|
readonly path: string;
|
|
40
43
|
constructor(module: RouteModule<any>, path: string);
|
|
41
|
-
resolve(ctx: GenericContext): Promise<Response | null>;
|
|
42
|
-
error(ctx: GenericContext, e: unknown): Promise<Response>;
|
|
43
|
-
private response;
|
|
44
44
|
}
|
|
45
45
|
export {};
|
package/dist/router.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ServerOnlyWarning } from "./internal/util.js";
|
|
2
2
|
ServerOnlyWarning("router");
|
|
3
3
|
import { MakeStatus } from "./status.js";
|
|
4
4
|
// builtin routes
|
|
@@ -59,6 +59,78 @@ export class RouteContext {
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
|
+
export class RouteResolver {
|
|
63
|
+
// using two seperate arrays to reduce object creation
|
|
64
|
+
// since these values are just pointers
|
|
65
|
+
stack;
|
|
66
|
+
slugs;
|
|
67
|
+
ctx;
|
|
68
|
+
constructor(ctx, fragments, tree) {
|
|
69
|
+
this.stack = [];
|
|
70
|
+
this.slugs = [];
|
|
71
|
+
this.ctx = ctx;
|
|
72
|
+
tree._applyChain(this, fragments, 0);
|
|
73
|
+
}
|
|
74
|
+
push(leaf, slug) {
|
|
75
|
+
this.stack.push(leaf);
|
|
76
|
+
this.slugs.push(slug);
|
|
77
|
+
}
|
|
78
|
+
async resolve() {
|
|
79
|
+
const pull = this.ctx.request.method === "HEAD" || this.ctx.request.method === "GET";
|
|
80
|
+
for (let i = this.stack.length - 1; i >= 0; i--) {
|
|
81
|
+
const node = this.stack[i];
|
|
82
|
+
// Determine the resolving function
|
|
83
|
+
const resolver = pull ? node.module.loader : node.module.action;
|
|
84
|
+
if (!resolver) {
|
|
85
|
+
if (pull)
|
|
86
|
+
continue;
|
|
87
|
+
return this.unwind(new Response("Method not Allowed", MakeStatus("Method Not Allowed", this.ctx.headers)), i);
|
|
88
|
+
}
|
|
89
|
+
// Apply the slug pseudo parameter if necessary
|
|
90
|
+
if (this.slugs[i])
|
|
91
|
+
this.ctx.params['$'] = this.slugs[i];
|
|
92
|
+
try {
|
|
93
|
+
const context = this.ctx.shape(node.module.parameters || {}, node.path);
|
|
94
|
+
const jsx = await resolver(context);
|
|
95
|
+
if (jsx === null)
|
|
96
|
+
continue;
|
|
97
|
+
if (jsx instanceof Response)
|
|
98
|
+
return jsx;
|
|
99
|
+
const res = this.ctx.render(jsx);
|
|
100
|
+
return html(res, { headers: this.ctx.headers });
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
return await this.unwind(e, i);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
async unwind(e, offset) {
|
|
109
|
+
for (let i = this.stack.length - 1; i >= 0; i--) {
|
|
110
|
+
const node = this.stack[i];
|
|
111
|
+
if (!node.module.error)
|
|
112
|
+
continue;
|
|
113
|
+
try {
|
|
114
|
+
const jsx = await node.module.error(this.ctx.shape({}, node.path), e);
|
|
115
|
+
let caught;
|
|
116
|
+
if (jsx instanceof Response)
|
|
117
|
+
caught = jsx;
|
|
118
|
+
else
|
|
119
|
+
caught = this.ctx.render(jsx);
|
|
120
|
+
if (caught instanceof Response) {
|
|
121
|
+
caught.headers.set("X-Caught", "true");
|
|
122
|
+
return caught;
|
|
123
|
+
}
|
|
124
|
+
this.ctx.headers.set("X-Caught", "true");
|
|
125
|
+
return html(caught, e instanceof Response ? e : MakeStatus("Internal Server Error", this.ctx.headers));
|
|
126
|
+
}
|
|
127
|
+
catch (next) {
|
|
128
|
+
e = next;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
throw e;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
62
134
|
export class RouteTree {
|
|
63
135
|
nested;
|
|
64
136
|
// Leaf nodes
|
|
@@ -105,75 +177,23 @@ export class RouteTree {
|
|
|
105
177
|
}
|
|
106
178
|
next.ingest(node, path.slice(1));
|
|
107
179
|
}
|
|
108
|
-
|
|
109
|
-
if (
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
return await this._resolve(fragments, ctx);
|
|
180
|
+
_applyChain(out, fragments, offset = 0) {
|
|
181
|
+
if (this.slug) {
|
|
182
|
+
const slug = fragments.slice(offset).join('/');
|
|
183
|
+
out.push(this.slug, slug);
|
|
113
184
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
async _resolve(fragments, ctx) {
|
|
119
|
-
let res = await this.resolveIndex(fragments, ctx)
|
|
120
|
-
|| await this.resolveNext(fragments, ctx)
|
|
121
|
-
|| await this.resolveWild(fragments, ctx)
|
|
122
|
-
|| await this.resolveSlug(fragments, ctx);
|
|
123
|
-
if (res instanceof Response) {
|
|
124
|
-
if (res.ok)
|
|
125
|
-
return res;
|
|
126
|
-
if (100 <= res.status && res.status <= 399)
|
|
127
|
-
return res;
|
|
128
|
-
if (res.headers.has("X-Caught"))
|
|
129
|
-
return res;
|
|
130
|
-
return this.unwrap(ctx, res);
|
|
185
|
+
if (offset === fragments.length) {
|
|
186
|
+
if (this.index)
|
|
187
|
+
out.push(this.index, undefined);
|
|
188
|
+
return;
|
|
131
189
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (fragments.length > 0)
|
|
136
|
-
return null;
|
|
137
|
-
if (!this.index)
|
|
138
|
-
return null;
|
|
139
|
-
const res = await this.index.resolve(ctx);
|
|
140
|
-
if (res instanceof Response)
|
|
141
|
-
return res;
|
|
142
|
-
if (res === null)
|
|
143
|
-
return null;
|
|
144
|
-
AssertUnreachable(res);
|
|
145
|
-
}
|
|
146
|
-
async resolveNext(fragments, ctx) {
|
|
147
|
-
if (fragments.length < 1)
|
|
148
|
-
return null;
|
|
149
|
-
const next = this.nested.get(fragments[0]);
|
|
150
|
-
if (!next)
|
|
151
|
-
return null;
|
|
152
|
-
return await next.resolve(fragments.slice(1), ctx);
|
|
153
|
-
}
|
|
154
|
-
async resolveWild(fragments, ctx) {
|
|
190
|
+
const keyed = this.nested.get(fragments[offset]);
|
|
191
|
+
if (keyed)
|
|
192
|
+
return keyed._applyChain(out, fragments, offset + 1);
|
|
155
193
|
if (!this.wild)
|
|
156
|
-
return
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
ctx.params[this.wildCard] = fragments[0];
|
|
160
|
-
return this.wild.resolve(fragments.slice(1), ctx);
|
|
161
|
-
}
|
|
162
|
-
async resolveSlug(fragments, ctx) {
|
|
163
|
-
if (!this.slug)
|
|
164
|
-
return null;
|
|
165
|
-
ctx.params["$"] = fragments.join("/");
|
|
166
|
-
const res = await this.slug.resolve(ctx);
|
|
167
|
-
if (res instanceof Response)
|
|
168
|
-
return res;
|
|
169
|
-
if (res === null)
|
|
170
|
-
return null;
|
|
171
|
-
AssertUnreachable(res);
|
|
172
|
-
}
|
|
173
|
-
unwrap(ctx, res) {
|
|
174
|
-
if (!this.slug)
|
|
175
|
-
throw res;
|
|
176
|
-
return this.slug.error(ctx, res);
|
|
194
|
+
return;
|
|
195
|
+
out.ctx.params[this.wildCard] = fragments[offset];
|
|
196
|
+
return this.wild._applyChain(out, fragments, offset + 1);
|
|
177
197
|
}
|
|
178
198
|
}
|
|
179
199
|
class RouteLeaf {
|
|
@@ -183,51 +203,4 @@ class RouteLeaf {
|
|
|
183
203
|
this.module = module;
|
|
184
204
|
this.path = path;
|
|
185
205
|
}
|
|
186
|
-
async resolve(ctx) {
|
|
187
|
-
const jsx = await this.response(ctx);
|
|
188
|
-
if (jsx === null)
|
|
189
|
-
return null;
|
|
190
|
-
if (jsx instanceof Response)
|
|
191
|
-
return jsx;
|
|
192
|
-
const res = ctx.render(jsx);
|
|
193
|
-
return html(res, { headers: ctx.headers });
|
|
194
|
-
}
|
|
195
|
-
async error(ctx, e) {
|
|
196
|
-
if (!this.module.error)
|
|
197
|
-
throw e;
|
|
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);
|
|
204
|
-
if (caught instanceof Response) {
|
|
205
|
-
caught.headers.set("X-Caught", "true");
|
|
206
|
-
return caught;
|
|
207
|
-
}
|
|
208
|
-
ctx.headers.set("X-Caught", "true");
|
|
209
|
-
return html(caught, e instanceof Response ? e : MakeStatus("Internal Server Error", ctx.headers));
|
|
210
|
-
}
|
|
211
|
-
async response(ctx) {
|
|
212
|
-
try {
|
|
213
|
-
if (!this.module.loader && !this.module.action)
|
|
214
|
-
return null;
|
|
215
|
-
const context = ctx.shape(this.module.parameters || {}, this.path);
|
|
216
|
-
if (ctx.request.method === "HEAD" || ctx.request.method === "GET") {
|
|
217
|
-
if (this.module.loader)
|
|
218
|
-
return await this.module.loader(context);
|
|
219
|
-
else
|
|
220
|
-
return null;
|
|
221
|
-
}
|
|
222
|
-
if (this.module.action)
|
|
223
|
-
return await this.module.action(context);
|
|
224
|
-
throw new Response("Method not Allowed", MakeStatus("Method Not Allowed", ctx.headers));
|
|
225
|
-
}
|
|
226
|
-
catch (e) {
|
|
227
|
-
if (e instanceof Response && e.headers.has("X-Caught"))
|
|
228
|
-
return e;
|
|
229
|
-
return await this.error(ctx, e);
|
|
230
|
-
}
|
|
231
|
-
return null;
|
|
232
|
-
}
|
|
233
206
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function Singleton<T>(name: string, cb: () => T): T;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Borrowed & modified from https://github.com/jenseng/abuse-the-platform/blob/main/app/utils/singleton.ts
|
|
2
|
+
export function Singleton(name, cb) {
|
|
3
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
4
|
+
const g = globalThis;
|
|
5
|
+
g.__singletons ??= {};
|
|
6
|
+
g.__singletons[name] ??= cb();
|
|
7
|
+
return g.__singletons[name];
|
|
8
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "htmx-router",
|
|
3
|
-
"version": "2.1
|
|
3
|
+
"version": "2.2.1",
|
|
4
4
|
"description": "A lightweight SSR framework with server+client islands",
|
|
5
5
|
"keywords": [ "htmx", "router", "client islands", "ssr", "vite" ],
|
|
6
6
|
"type": "module",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"./css": "./dist/css.js",
|
|
15
15
|
"./defer": "./dist/defer.js",
|
|
16
16
|
"./endpoint": "./dist/endpoint.js",
|
|
17
|
+
"./event": "./dist/event.js",
|
|
17
18
|
"./event-source": "./dist/event-source.js",
|
|
18
19
|
"./navigate": "./dist/navigate.js",
|
|
19
20
|
"./response": "./dist/response.js",
|