htmx-router 1.0.4 → 1.0.6
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/css.js +3 -4
- package/event-source.js +10 -9
- package/internal/mount.js +3 -4
- package/internal/request/http.d.ts +1 -1
- package/internal/request/native.d.ts +6 -1
- package/internal/request/native.js +30 -29
- package/internal/util.d.ts +1 -0
- package/internal/util.js +4 -0
- package/package.json +1 -1
- package/response.d.ts +2 -2
- package/router.js +23 -23
- package/status.d.ts +1 -4
- package/status.js +17 -6
package/css.js
CHANGED
|
@@ -53,8 +53,7 @@ export async function loader(ctx) {
|
|
|
53
53
|
const build = GetSheet();
|
|
54
54
|
if (!ctx.params.hash.startsWith(build.hash))
|
|
55
55
|
return null;
|
|
56
|
-
|
|
57
|
-
headers.set("
|
|
58
|
-
|
|
59
|
-
return new Response(build.sheet, { headers });
|
|
56
|
+
ctx.headers.set("Content-Type", "text/css");
|
|
57
|
+
ctx.headers.set("Cache-Control", "public, max-age=604800");
|
|
58
|
+
return new Response(build.sheet, { headers: ctx.headers });
|
|
60
59
|
}
|
package/event-source.js
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { ServerOnlyWarning } from "./internal/util.js";
|
|
2
2
|
ServerOnlyWarning("event-source");
|
|
3
|
+
// global for easy reuse
|
|
4
|
+
const encoder = new TextEncoder();
|
|
5
|
+
const headers = new Headers();
|
|
6
|
+
// Chunked encoding with immediate forwarding by proxies (i.e. nginx)
|
|
7
|
+
headers.set("X-Accel-Buffering", "no");
|
|
8
|
+
headers.set("Transfer-Encoding", "chunked");
|
|
9
|
+
headers.set("Content-Type", "text/event-stream");
|
|
10
|
+
// the maximum keep alive chrome shouldn't ignore
|
|
11
|
+
headers.set("Keep-Alive", "timeout=120");
|
|
12
|
+
headers.set("Connection", "keep-alive");
|
|
3
13
|
/**
|
|
4
14
|
* Helper for Server-Sent-Events, with auto close on SIGTERM and SIGHUP messages
|
|
5
15
|
* Includes a keep alive empty packet sent every 30sec (because Chrome implodes at 120sec, and can be unreliable at 60sec)
|
|
@@ -95,15 +105,6 @@ export class EventSourceSet extends Set {
|
|
|
95
105
|
this.clear();
|
|
96
106
|
}
|
|
97
107
|
}
|
|
98
|
-
// global for easy reuse
|
|
99
|
-
const encoder = new TextEncoder();
|
|
100
|
-
const headers = new Headers();
|
|
101
|
-
// Chunked encoding with immediate forwarding by proxies (i.e. nginx)
|
|
102
|
-
headers.set("X-Accel-Buffering", "no");
|
|
103
|
-
headers.set("Transfer-Encoding", "chunked");
|
|
104
|
-
headers.set("Content-Type", "text/event-stream");
|
|
105
|
-
headers.set("Keep-Alive", "timeout=120"); // the maximum keep alive chrome shouldn't ignore
|
|
106
|
-
headers.set("Connection", "keep-alive");
|
|
107
108
|
// Auto close all SSE streams when shutdown requested
|
|
108
109
|
// Without this graceful shutdowns will hang indefinitely
|
|
109
110
|
const register = new EventSourceSet();
|
package/internal/mount.js
CHANGED
|
@@ -120,8 +120,7 @@ export async function loader(ctx) {
|
|
|
120
120
|
// const build = GetSheet();
|
|
121
121
|
if (!ctx.params.hash.startsWith(hash))
|
|
122
122
|
return null;
|
|
123
|
-
|
|
124
|
-
headers.set("
|
|
125
|
-
|
|
126
|
-
return new Response(script, { headers });
|
|
123
|
+
ctx.headers.set("Content-Type", "text/javascript");
|
|
124
|
+
ctx.headers.set("Cache-Control", "public, max-age=604800");
|
|
125
|
+
return new Response(script, { headers: ctx.headers });
|
|
127
126
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "http";
|
|
2
2
|
import type { ViteDevServer } from "vite";
|
|
3
|
-
import
|
|
3
|
+
import { GenericContext } from "../router.js";
|
|
4
4
|
type Config = {
|
|
5
5
|
build: Promise<any> | (() => Promise<Record<string, any>>);
|
|
6
6
|
viteDevServer: ViteDevServer | null;
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { Config } from './index.js';
|
|
2
2
|
import type { RouteTree } from '../../router.js';
|
|
3
|
-
export declare function createRequestHandler(config: Config): (req: Request) => Promise<
|
|
3
|
+
export declare function createRequestHandler(config: Config): (req: Request) => Promise<{
|
|
4
|
+
response: Response;
|
|
5
|
+
headers: {
|
|
6
|
+
[key: string]: string | string[];
|
|
7
|
+
};
|
|
8
|
+
}>;
|
|
4
9
|
export declare function Resolve(request: Request, tree: RouteTree, config: Config): Promise<{
|
|
5
10
|
response: Response;
|
|
6
11
|
headers: {
|
|
@@ -1,40 +1,41 @@
|
|
|
1
1
|
import { ServerOnlyWarning } from "../util.js";
|
|
2
2
|
ServerOnlyWarning("native-request");
|
|
3
3
|
import { GenericContext } from "../router.js";
|
|
4
|
+
import { MakeStatus } from "../../status.js";
|
|
4
5
|
export function createRequestHandler(config) {
|
|
5
6
|
return async (req) => {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
let { response } = await Resolve(req, mod.tree, config);
|
|
9
|
-
return response;
|
|
10
|
-
}
|
|
11
|
-
catch (e) {
|
|
12
|
-
if (e instanceof Error) {
|
|
13
|
-
console.error(e.stack);
|
|
14
|
-
config.viteDevServer?.ssrFixStacktrace(e);
|
|
15
|
-
return new Response(e.message + "\n" + e.stack, { status: 500, statusText: "Internal Server Error" });
|
|
16
|
-
}
|
|
17
|
-
else {
|
|
18
|
-
console.error(e);
|
|
19
|
-
return new Response(String(e), { status: 500, statusText: "Internal Server Error" });
|
|
20
|
-
}
|
|
21
|
-
}
|
|
7
|
+
const mod = typeof config.build === "function" ? await config.build() : await config.build;
|
|
8
|
+
return await Resolve(req, mod.tree, config);
|
|
22
9
|
};
|
|
23
10
|
}
|
|
24
11
|
export async function Resolve(request, tree, config) {
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
response =
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
12
|
+
const ctx = new GenericContext(request, new URL(request.url), config.render);
|
|
13
|
+
let response;
|
|
14
|
+
try {
|
|
15
|
+
const x = ctx.url.pathname.endsWith("/") ? ctx.url.pathname.slice(0, -1) : ctx.url.pathname;
|
|
16
|
+
const fragments = x.split("/").slice(1);
|
|
17
|
+
const res = await tree.resolve(fragments, ctx);
|
|
18
|
+
response = res === null
|
|
19
|
+
? new Response("No Route Found", MakeStatus("Not Found", ctx.headers))
|
|
20
|
+
: res;
|
|
21
|
+
// Override with context headers
|
|
22
|
+
if (response.headers !== ctx.headers) {
|
|
23
|
+
for (const [key, value] of ctx.headers) {
|
|
24
|
+
if (key === "content-type")
|
|
25
|
+
continue;
|
|
26
|
+
response.headers.set(key, value);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
if (e instanceof Error) {
|
|
32
|
+
console.error(e.stack);
|
|
33
|
+
config.viteDevServer?.ssrFixStacktrace(e);
|
|
34
|
+
response = new Response(e.message + "\n" + e.stack, { status: 500, statusText: "Internal Server Error" });
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
console.error(e);
|
|
38
|
+
response = new Response(String(e), { status: 500, statusText: "Internal Server Error" });
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
41
|
// Merge cookie changes
|
package/internal/util.d.ts
CHANGED
|
@@ -2,3 +2,4 @@ export declare function QuickHash(input: string): string;
|
|
|
2
2
|
export declare function CutString(str: string, pivot: string, offset?: number): [string, string];
|
|
3
3
|
export declare function Singleton<T>(name: string, cb: () => T): T;
|
|
4
4
|
export declare function ServerOnlyWarning(context: string): void;
|
|
5
|
+
export declare function AssertUnreachable(x: never): never;
|
package/internal/util.js
CHANGED
|
@@ -47,3 +47,7 @@ export function ServerOnlyWarning(context) {
|
|
|
47
47
|
console.warn(`Warn: Server-side only htmx-router feature ${context} has leaked to client code`);
|
|
48
48
|
console.log(typeof document, typeof process);
|
|
49
49
|
}
|
|
50
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
51
|
+
export function AssertUnreachable(x) {
|
|
52
|
+
throw new Error("Unreachable code path reachable");
|
|
53
|
+
}
|
package/package.json
CHANGED
package/response.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export declare function text(text:
|
|
2
|
-
export declare function html(text:
|
|
1
|
+
export declare function text(text: BodyInit, init?: ResponseInit): Response;
|
|
2
|
+
export declare function html(text: BodyInit, init?: ResponseInit): Response;
|
|
3
3
|
export type TypedResponse<T> = Omit<Response, "json"> & {
|
|
4
4
|
json(): Promise<T>;
|
|
5
5
|
};
|
package/router.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { ServerOnlyWarning } from "./internal/util.js";
|
|
1
|
+
import { AssertUnreachable, ServerOnlyWarning } from "./internal/util.js";
|
|
2
2
|
ServerOnlyWarning("router");
|
|
3
|
+
import { MakeStatus } from "./status.js";
|
|
3
4
|
// builtin routes
|
|
4
5
|
import * as endpoint from './endpoint.js';
|
|
5
6
|
import * as dynamic from './defer.js';
|
|
6
7
|
import * as mount from './internal/mount.js';
|
|
7
8
|
import * as css from './css.js';
|
|
9
|
+
import { html } from "./response.js";
|
|
8
10
|
export function GenerateRouteTree(props) {
|
|
9
11
|
if (!props.scope.endsWith("/"))
|
|
10
12
|
props.scope += "/";
|
|
@@ -134,7 +136,7 @@ export class RouteTree {
|
|
|
134
136
|
return res;
|
|
135
137
|
if (res === null)
|
|
136
138
|
return null;
|
|
137
|
-
|
|
139
|
+
AssertUnreachable(res);
|
|
138
140
|
}
|
|
139
141
|
async resolveNext(fragments, ctx) {
|
|
140
142
|
if (fragments.length < 1)
|
|
@@ -161,22 +163,12 @@ export class RouteTree {
|
|
|
161
163
|
return res;
|
|
162
164
|
if (res === null)
|
|
163
165
|
return null;
|
|
164
|
-
|
|
166
|
+
AssertUnreachable(res);
|
|
165
167
|
}
|
|
166
|
-
|
|
168
|
+
unwrap(ctx, res) {
|
|
167
169
|
if (!this.slug)
|
|
168
170
|
throw res;
|
|
169
|
-
|
|
170
|
-
if (caught instanceof Response) {
|
|
171
|
-
caught.headers.set("X-Caught", "true");
|
|
172
|
-
return caught;
|
|
173
|
-
}
|
|
174
|
-
ctx.headers.set("X-Caught", "true");
|
|
175
|
-
return new Response(caught, res instanceof Response ? res : {
|
|
176
|
-
status: 500,
|
|
177
|
-
statusText: "Internal Server Error",
|
|
178
|
-
headers: ctx.headers
|
|
179
|
-
});
|
|
171
|
+
return this.slug.error(ctx, res);
|
|
180
172
|
}
|
|
181
173
|
}
|
|
182
174
|
class RouteLeaf {
|
|
@@ -185,20 +177,28 @@ class RouteLeaf {
|
|
|
185
177
|
this.module = module;
|
|
186
178
|
}
|
|
187
179
|
async resolve(ctx) {
|
|
188
|
-
const
|
|
189
|
-
if (
|
|
180
|
+
const jsx = await this.response(ctx);
|
|
181
|
+
if (jsx === null)
|
|
190
182
|
return null;
|
|
183
|
+
if (jsx instanceof Response)
|
|
184
|
+
return jsx;
|
|
185
|
+
const res = await ctx.render(jsx, ctx.headers);
|
|
191
186
|
if (res instanceof Response)
|
|
192
187
|
return res;
|
|
193
|
-
return
|
|
188
|
+
return html(res, { headers: ctx.headers });
|
|
194
189
|
}
|
|
195
190
|
async error(ctx, e) {
|
|
196
191
|
if (!this.module.error)
|
|
197
192
|
throw e;
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
193
|
+
let jsx = await this.module.error(ctx, e);
|
|
194
|
+
const caught = jsx instanceof Response ? jsx
|
|
195
|
+
: await ctx.render(jsx, ctx.headers);
|
|
196
|
+
if (caught instanceof Response) {
|
|
197
|
+
caught.headers.set("X-Caught", "true");
|
|
198
|
+
return caught;
|
|
199
|
+
}
|
|
200
|
+
ctx.headers.set("X-Caught", "true");
|
|
201
|
+
return html(caught, e instanceof Response ? e : MakeStatus("Internal Server Error", ctx.headers));
|
|
202
202
|
}
|
|
203
203
|
async response(ctx) {
|
|
204
204
|
try {
|
|
@@ -213,7 +213,7 @@ class RouteLeaf {
|
|
|
213
213
|
}
|
|
214
214
|
if (this.module.action)
|
|
215
215
|
return await this.module.action(context);
|
|
216
|
-
throw new Response("Method not Allowed",
|
|
216
|
+
throw new Response("Method not Allowed", MakeStatus("Method Not Allowed", ctx.headers));
|
|
217
217
|
}
|
|
218
218
|
catch (e) {
|
|
219
219
|
return await this.error(ctx, e);
|
package/status.d.ts
CHANGED
|
@@ -64,8 +64,5 @@ declare const definitions: {
|
|
|
64
64
|
511: "Network Authentication Required";
|
|
65
65
|
};
|
|
66
66
|
export type StatusText = typeof definitions[keyof typeof definitions];
|
|
67
|
-
export declare function MakeStatus(lookup: number | StatusText):
|
|
68
|
-
status: number;
|
|
69
|
-
statusText: StatusText;
|
|
70
|
-
};
|
|
67
|
+
export declare function MakeStatus(lookup: number | StatusText, init?: ResponseInit | Headers): ResponseInit;
|
|
71
68
|
export {};
|
package/status.js
CHANGED
|
@@ -73,21 +73,32 @@ for (const key in definitions) {
|
|
|
73
73
|
const code = Number(key);
|
|
74
74
|
index.set(definitions[code].toLowerCase(), code);
|
|
75
75
|
}
|
|
76
|
-
export function MakeStatus(lookup) {
|
|
76
|
+
export function MakeStatus(lookup, init) {
|
|
77
|
+
if (init instanceof Headers)
|
|
78
|
+
init = { headers: init };
|
|
77
79
|
if (typeof lookup === "number")
|
|
78
|
-
return lookupCode(lookup);
|
|
79
|
-
return lookupStatus(lookup);
|
|
80
|
+
return lookupCode(lookup, init);
|
|
81
|
+
return lookupStatus(lookup, init);
|
|
80
82
|
}
|
|
81
|
-
function lookupCode(status) {
|
|
83
|
+
function lookupCode(status, init) {
|
|
82
84
|
if (status < 100)
|
|
83
85
|
throw new TypeError(`Status ${status}<100`);
|
|
84
86
|
if (status > 599)
|
|
85
87
|
throw new TypeError(`Status ${status}>599`);
|
|
86
|
-
|
|
88
|
+
const statusText = lookup[status];
|
|
89
|
+
return Status(status, statusText, init);
|
|
87
90
|
}
|
|
88
|
-
function lookupStatus(statusText) {
|
|
91
|
+
function lookupStatus(statusText, init) {
|
|
89
92
|
const status = index.get(statusText.toLowerCase());
|
|
90
93
|
if (!status)
|
|
91
94
|
throw new TypeError(`statusText ${statusText} is not of type StatusText`);
|
|
95
|
+
return Status(status, statusText, init);
|
|
96
|
+
}
|
|
97
|
+
function Status(status, statusText, init) {
|
|
98
|
+
if (init) {
|
|
99
|
+
init.statusText = statusText;
|
|
100
|
+
init.status = status;
|
|
101
|
+
return init;
|
|
102
|
+
}
|
|
92
103
|
return { status, statusText };
|
|
93
104
|
}
|