htmx-router 1.0.17 → 2.0.0
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/index.d.ts +1 -2
- package/index.js +1 -2
- package/internal/request/compatibility/node.d.ts +3 -0
- package/internal/request/compatibility/node.js +54 -0
- package/internal/request/compatibility/vite/connectToWeb.d.ts +13 -0
- package/internal/request/compatibility/vite/connectToWeb.js +59 -0
- package/internal/request/compatibility/vite/createServerResponse.d.ts +24 -0
- package/internal/request/compatibility/vite/createServerResponse.js +63 -0
- package/internal/request/compatibility/vite/header-utils.d.ts +6 -0
- package/internal/request/compatibility/vite/header-utils.js +70 -0
- package/internal/request/http.d.ts +3 -0
- package/internal/request/http.js +5 -23
- package/internal/request/index.d.ts +0 -13
- package/internal/request/index.js +0 -6
- package/internal/request/native.d.ts +6 -0
- package/internal/request/native.js +6 -0
- package/internal/request/server.d.ts +37 -0
- package/internal/request/server.js +144 -0
- package/package.json +1 -1
- package/router.d.ts +1 -1
- package/server.d.ts +2 -0
- package/server.js +4 -0
- package/status.d.ts +0 -4
- package/status.js +1 -0
package/index.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { ParameterShaper } from "./util/parameters.js";
|
|
2
2
|
import type { RouteContext } from "./router.js";
|
|
3
|
-
import { createRequestHandler } from "./internal/request/index.js";
|
|
4
3
|
export type RenderFunction<T extends ParameterShaper = {}> = (ctx: RouteContext<T>) => Promise<Response | JSX.Element | null>;
|
|
5
4
|
export type CatchFunction<T extends ParameterShaper = {}> = (ctx: RouteContext<T>, err: unknown) => Promise<Response | JSX.Element>;
|
|
6
5
|
export type RouteModule<T extends ParameterShaper> = {
|
|
@@ -16,4 +15,4 @@ export type ClientIslandManifest<T> = {
|
|
|
16
15
|
type ClientIsland<T> = T extends (props: infer P) => JSX.Element ? (props: P & {
|
|
17
16
|
children?: JSX.Element;
|
|
18
17
|
}) => JSX.Element : T;
|
|
19
|
-
export {
|
|
18
|
+
export { RouteContext };
|
package/index.js
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export { createRequestHandler };
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export function NodeAdaptor(server, resolve404) {
|
|
2
|
+
return async (req, res) => {
|
|
3
|
+
const request = CreateRequest(req);
|
|
4
|
+
const response = await server.resolve(request, resolve404);
|
|
5
|
+
if (response === null)
|
|
6
|
+
return;
|
|
7
|
+
ConsumeResponse(res, response);
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
function CreateRequest(req) {
|
|
11
|
+
const ctrl = new AbortController();
|
|
12
|
+
const headers = new Headers(req.headers);
|
|
13
|
+
const url = new URL(`http://${headers.get('host')}${req.originalUrl || req.url}`);
|
|
14
|
+
req.once('aborted', () => ctrl.abort());
|
|
15
|
+
const bodied = req.method !== "GET" && req.method !== "HEAD";
|
|
16
|
+
const request = new Request(url, {
|
|
17
|
+
headers,
|
|
18
|
+
method: req.method,
|
|
19
|
+
body: bodied ? req : undefined,
|
|
20
|
+
signal: ctrl.signal,
|
|
21
|
+
referrer: headers.get("referrer") || undefined,
|
|
22
|
+
// @ts-ignore
|
|
23
|
+
duplex: bodied ? 'half' : undefined
|
|
24
|
+
});
|
|
25
|
+
if (!request.headers.has("X-Real-IP")) {
|
|
26
|
+
const info = req.socket.address();
|
|
27
|
+
if ("address" in info)
|
|
28
|
+
request.headers.set("X-Real-IP", info.address);
|
|
29
|
+
}
|
|
30
|
+
return request;
|
|
31
|
+
}
|
|
32
|
+
async function ConsumeResponse(into, response) {
|
|
33
|
+
const headers = Object.fromEntries(response.headers);
|
|
34
|
+
{ // handle multi-cookie setting
|
|
35
|
+
const cookies = response.headers.getSetCookie();
|
|
36
|
+
if (cookies.length > 0)
|
|
37
|
+
headers["set-cookie"] = cookies;
|
|
38
|
+
}
|
|
39
|
+
into.writeHead(response.status, headers);
|
|
40
|
+
if (response.body instanceof ReadableStream) {
|
|
41
|
+
const reader = response.body.getReader();
|
|
42
|
+
while (true) {
|
|
43
|
+
const { done, value } = await reader.read();
|
|
44
|
+
if (done)
|
|
45
|
+
break;
|
|
46
|
+
into.write(value);
|
|
47
|
+
}
|
|
48
|
+
into.end();
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
const rendered = await response.text();
|
|
52
|
+
into.end(rendered);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
export { connectToWeb };
|
|
3
|
+
import { GenericContext } from '../../../router.js';
|
|
4
|
+
type NextFunction = (err?: unknown) => void;
|
|
5
|
+
type ConnectMiddleware<PlatformRequest extends IncomingMessage = IncomingMessage, PlatformResponse extends ServerResponse = ServerResponse> = (req: PlatformRequest, res: PlatformResponse, next: NextFunction) => void;
|
|
6
|
+
type WebHandler = (ctx: GenericContext) => Promise<Response | null>;
|
|
7
|
+
/**
|
|
8
|
+
* Converts a Connect-style middleware to a web-compatible request handler.
|
|
9
|
+
*
|
|
10
|
+
* @param {ConnectMiddleware} handler - The Connect-style middleware function to be converted.
|
|
11
|
+
* @returns {WebHandler} A function that handles web requests and returns a Response or undefined.
|
|
12
|
+
*/
|
|
13
|
+
declare function connectToWeb(handler: ConnectMiddleware): WebHandler;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Readable } from 'node:stream';
|
|
2
|
+
export { connectToWeb };
|
|
3
|
+
import { createServerResponse } from './createServerResponse.js';
|
|
4
|
+
import { flattenHeaders } from './header-utils.js';
|
|
5
|
+
const statusCodesWithoutBody = [
|
|
6
|
+
100, // Continue
|
|
7
|
+
101, // Switching Protocols
|
|
8
|
+
102, // Processing (WebDAV)
|
|
9
|
+
103, // Early Hints
|
|
10
|
+
204, // No Content
|
|
11
|
+
205, // Reset Content
|
|
12
|
+
304, // Not Modified
|
|
13
|
+
];
|
|
14
|
+
/**
|
|
15
|
+
* Converts a Connect-style middleware to a web-compatible request handler.
|
|
16
|
+
*
|
|
17
|
+
* @param {ConnectMiddleware} handler - The Connect-style middleware function to be converted.
|
|
18
|
+
* @returns {WebHandler} A function that handles web requests and returns a Response or undefined.
|
|
19
|
+
*/
|
|
20
|
+
function connectToWeb(handler) {
|
|
21
|
+
return async (ctx) => {
|
|
22
|
+
const req = createIncomingMessage(ctx.request);
|
|
23
|
+
const { res, onReadable } = createServerResponse(req);
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
onReadable(({ readable, headers, statusCode }) => {
|
|
26
|
+
const responseBody = statusCodesWithoutBody.includes(statusCode)
|
|
27
|
+
? null
|
|
28
|
+
: Readable.toWeb(readable);
|
|
29
|
+
resolve(new Response(responseBody, {
|
|
30
|
+
status: statusCode,
|
|
31
|
+
headers: flattenHeaders(headers)
|
|
32
|
+
}));
|
|
33
|
+
});
|
|
34
|
+
const next = (error) => {
|
|
35
|
+
if (error)
|
|
36
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
37
|
+
else
|
|
38
|
+
resolve(null);
|
|
39
|
+
};
|
|
40
|
+
Promise.resolve(handler(req, res, next)).catch(next);
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Creates an IncomingMessage object from a web Request.
|
|
46
|
+
*
|
|
47
|
+
* @param {Request} request - The web Request object.
|
|
48
|
+
* @returns {IncomingMessage} An IncomingMessage-like object compatible with Node.js HTTP module.
|
|
49
|
+
*/
|
|
50
|
+
function createIncomingMessage(request) {
|
|
51
|
+
const parsedUrl = new URL(request.url, 'http://localhost');
|
|
52
|
+
const pathnameAndQuery = (parsedUrl.pathname || '') + (parsedUrl.search || '');
|
|
53
|
+
const body = request.body ? Readable.fromWeb(request.body) : Readable.from([]);
|
|
54
|
+
return Object.assign(body, {
|
|
55
|
+
url: pathnameAndQuery,
|
|
56
|
+
method: request.method,
|
|
57
|
+
headers: Object.fromEntries(request.headers)
|
|
58
|
+
});
|
|
59
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export { createServerResponse };
|
|
2
|
+
import { ServerResponse, type IncomingMessage, type OutgoingHttpHeaders } from 'node:http';
|
|
3
|
+
import { Readable } from 'node:stream';
|
|
4
|
+
/**
|
|
5
|
+
* Creates a custom ServerResponse object that allows for intercepting and streaming the response.
|
|
6
|
+
*
|
|
7
|
+
* @param {IncomingMessage} incomingMessage - The incoming HTTP request message.
|
|
8
|
+
* @returns {{
|
|
9
|
+
* res: ServerResponse;
|
|
10
|
+
* onReadable: (cb: (result: { readable: Readable; headers: OutgoingHttpHeaders; statusCode: number }) => void) => void
|
|
11
|
+
* }}
|
|
12
|
+
* An object containing:
|
|
13
|
+
* - res: The custom ServerResponse object.
|
|
14
|
+
* - onReadable: A function that takes a callback. The callback is invoked when the response is readable,
|
|
15
|
+
* providing an object with the readable stream, headers, and status code.
|
|
16
|
+
*/
|
|
17
|
+
declare function createServerResponse(incomingMessage: IncomingMessage): {
|
|
18
|
+
res: ServerResponse<IncomingMessage>;
|
|
19
|
+
onReadable: (cb: (result: {
|
|
20
|
+
readable: Readable;
|
|
21
|
+
headers: OutgoingHttpHeaders;
|
|
22
|
+
statusCode: number;
|
|
23
|
+
}) => void) => void;
|
|
24
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Taken From: https://github.com/vikejs/vike-server/blob/7bcb7805fa39ef945adc73f4faf5c89a81f3f7ec/packages/vike-node/src/runtime/adapters/createServerResponse.ts
|
|
2
|
+
export { createServerResponse };
|
|
3
|
+
import { ServerResponse } from 'node:http';
|
|
4
|
+
import { PassThrough, Readable } from 'node:stream';
|
|
5
|
+
/**
|
|
6
|
+
* Creates a custom ServerResponse object that allows for intercepting and streaming the response.
|
|
7
|
+
*
|
|
8
|
+
* @param {IncomingMessage} incomingMessage - The incoming HTTP request message.
|
|
9
|
+
* @returns {{
|
|
10
|
+
* res: ServerResponse;
|
|
11
|
+
* onReadable: (cb: (result: { readable: Readable; headers: OutgoingHttpHeaders; statusCode: number }) => void) => void
|
|
12
|
+
* }}
|
|
13
|
+
* An object containing:
|
|
14
|
+
* - res: The custom ServerResponse object.
|
|
15
|
+
* - onReadable: A function that takes a callback. The callback is invoked when the response is readable,
|
|
16
|
+
* providing an object with the readable stream, headers, and status code.
|
|
17
|
+
*/
|
|
18
|
+
function createServerResponse(incomingMessage) {
|
|
19
|
+
const res = new ServerResponse(incomingMessage);
|
|
20
|
+
const passThrough = new PassThrough();
|
|
21
|
+
let handled = false;
|
|
22
|
+
const onReadable = (cb) => {
|
|
23
|
+
const handleReadable = () => {
|
|
24
|
+
if (handled)
|
|
25
|
+
return;
|
|
26
|
+
handled = true;
|
|
27
|
+
cb({ readable: Readable.from(passThrough), headers: res.getHeaders(), statusCode: res.statusCode });
|
|
28
|
+
};
|
|
29
|
+
passThrough.once('readable', handleReadable);
|
|
30
|
+
passThrough.once('end', handleReadable);
|
|
31
|
+
};
|
|
32
|
+
passThrough.once('finish', () => {
|
|
33
|
+
res.emit('finish');
|
|
34
|
+
});
|
|
35
|
+
passThrough.once('close', () => {
|
|
36
|
+
res.destroy();
|
|
37
|
+
res.emit('close');
|
|
38
|
+
});
|
|
39
|
+
passThrough.on('drain', () => {
|
|
40
|
+
res.emit('drain');
|
|
41
|
+
});
|
|
42
|
+
res.write = passThrough.write.bind(passThrough);
|
|
43
|
+
res.end = passThrough.end.bind(passThrough);
|
|
44
|
+
res.writeHead = function writeHead(statusCode, statusMessage, headers) {
|
|
45
|
+
res.statusCode = statusCode;
|
|
46
|
+
if (typeof statusMessage === 'object') {
|
|
47
|
+
headers = statusMessage;
|
|
48
|
+
statusMessage = undefined;
|
|
49
|
+
}
|
|
50
|
+
if (headers) {
|
|
51
|
+
Object.entries(headers).forEach(([key, value]) => {
|
|
52
|
+
if (value !== undefined) {
|
|
53
|
+
res.setHeader(key, value);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return res;
|
|
58
|
+
};
|
|
59
|
+
return {
|
|
60
|
+
res,
|
|
61
|
+
onReadable
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { flattenHeaders, groupHeaders, parseHeaders };
|
|
2
|
+
import type { OutgoingHttpHeaders } from 'node:http';
|
|
3
|
+
type HeadersProvided = Record<string, string | string[] | undefined> | Headers;
|
|
4
|
+
declare function groupHeaders(headers: [string, string][]): [string, string | string[]][];
|
|
5
|
+
declare function flattenHeaders(headers: OutgoingHttpHeaders): [string, string][];
|
|
6
|
+
declare function parseHeaders(headers: HeadersProvided): [string, string][];
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Taken From: https://github.com/vikejs/vike-server/blob/7bcb7805fa39ef945adc73f4faf5c89a81f3f7ec/packages/vike-node/src/runtime/utils/header-utils.ts
|
|
2
|
+
export { flattenHeaders, groupHeaders, parseHeaders };
|
|
3
|
+
function groupHeaders(headers) {
|
|
4
|
+
const grouped = {};
|
|
5
|
+
headers.forEach(([key, value]) => {
|
|
6
|
+
if (grouped[key]) {
|
|
7
|
+
// If the key already exists, append the new value
|
|
8
|
+
if (Array.isArray(grouped[key])) {
|
|
9
|
+
;
|
|
10
|
+
grouped[key].push(value);
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
grouped[key] = [grouped[key], value];
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
// If the key doesn't exist, add it to the object
|
|
18
|
+
grouped[key] = value;
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
// Convert the object back to an array
|
|
22
|
+
return Object.entries(grouped);
|
|
23
|
+
}
|
|
24
|
+
function flattenHeaders(headers) {
|
|
25
|
+
const flatHeaders = [];
|
|
26
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
27
|
+
if (value === undefined || value === null) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (Array.isArray(value)) {
|
|
31
|
+
value.forEach((v) => {
|
|
32
|
+
if (v != null) {
|
|
33
|
+
flatHeaders.push([key, String(v)]);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
flatHeaders.push([key, String(value)]);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return flatHeaders;
|
|
42
|
+
}
|
|
43
|
+
function parseHeaders(headers) {
|
|
44
|
+
const result = [];
|
|
45
|
+
if (typeof headers.forEach === 'function') {
|
|
46
|
+
headers.forEach((value, key) => {
|
|
47
|
+
if (Array.isArray(value)) {
|
|
48
|
+
value.forEach((value_) => {
|
|
49
|
+
result.push([key, value_]);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
result.push([key, value]);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
59
|
+
if (Array.isArray(value)) {
|
|
60
|
+
value.forEach((value_) => {
|
|
61
|
+
result.push([key, value_]);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
result.push([key, value]);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
}
|
package/internal/request/http.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { ServerOnlyWarning } from "../util.js";
|
|
2
2
|
ServerOnlyWarning("http-request");
|
|
3
|
+
import { CreateRequest } from "./compatibility/node.js";
|
|
3
4
|
import { Resolve } from "./native.js";
|
|
5
|
+
/**
|
|
6
|
+
* @deprecated
|
|
7
|
+
*/
|
|
4
8
|
export function createRequestHandler(config) {
|
|
5
9
|
return async (req, res) => {
|
|
6
10
|
try {
|
|
7
11
|
const mod = typeof config.build === "function" ? await config.build() : await config.build;
|
|
8
|
-
const request =
|
|
12
|
+
const request = CreateRequest(req);
|
|
9
13
|
let { response, headers } = await Resolve(request, mod.tree, config);
|
|
10
14
|
res.writeHead(response.status, headers);
|
|
11
15
|
if (response.body instanceof ReadableStream) {
|
|
@@ -37,25 +41,3 @@ export function createRequestHandler(config) {
|
|
|
37
41
|
}
|
|
38
42
|
};
|
|
39
43
|
}
|
|
40
|
-
function NativeRequest(req) {
|
|
41
|
-
const ctrl = new AbortController();
|
|
42
|
-
const headers = new Headers(req.headers);
|
|
43
|
-
const url = new URL(`http://${headers.get('host')}${req.originalUrl || req.url}`);
|
|
44
|
-
req.once('aborted', () => ctrl.abort());
|
|
45
|
-
const bodied = req.method !== "GET" && req.method !== "HEAD";
|
|
46
|
-
const request = new Request(url, {
|
|
47
|
-
headers,
|
|
48
|
-
method: req.method,
|
|
49
|
-
body: bodied ? req : undefined,
|
|
50
|
-
signal: ctrl.signal,
|
|
51
|
-
referrer: headers.get("referrer") || undefined,
|
|
52
|
-
// @ts-ignore
|
|
53
|
-
duplex: bodied ? 'half' : undefined
|
|
54
|
-
});
|
|
55
|
-
if (!request.headers.has("X-Real-IP")) {
|
|
56
|
-
const info = req.socket.address();
|
|
57
|
-
if ("address" in info)
|
|
58
|
-
request.headers.set("X-Real-IP", info.address);
|
|
59
|
-
}
|
|
60
|
-
return request;
|
|
61
|
-
}
|
|
@@ -1,17 +1,4 @@
|
|
|
1
|
-
import type { ViteDevServer } from "vite";
|
|
2
|
-
import type { GenericContext } from "../router.js";
|
|
3
1
|
import type { RouteTree } from '../../router.js';
|
|
4
|
-
import * as native from "./native.js";
|
|
5
|
-
import * as http from "./http.js";
|
|
6
|
-
export type Config = {
|
|
7
|
-
build: Promise<any> | (() => Promise<Record<string, any>>);
|
|
8
|
-
viteDevServer: ViteDevServer | null;
|
|
9
|
-
render: GenericContext["render"];
|
|
10
|
-
};
|
|
11
2
|
export type RouterModule = {
|
|
12
3
|
tree: RouteTree;
|
|
13
4
|
};
|
|
14
|
-
export declare const createRequestHandler: {
|
|
15
|
-
http: typeof http.createRequestHandler;
|
|
16
|
-
native: typeof native.createRequestHandler;
|
|
17
|
-
};
|
|
@@ -1,8 +1,2 @@
|
|
|
1
1
|
import { ServerOnlyWarning } from "../util.js";
|
|
2
2
|
ServerOnlyWarning("request");
|
|
3
|
-
import * as native from "./native.js";
|
|
4
|
-
import * as http from "./http.js";
|
|
5
|
-
export const createRequestHandler = {
|
|
6
|
-
http: http.createRequestHandler,
|
|
7
|
-
native: native.createRequestHandler
|
|
8
|
-
};
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import type { Config } from './index.js';
|
|
2
2
|
import type { RouteTree } from '../../router.js';
|
|
3
|
+
/**
|
|
4
|
+
* @deprecated
|
|
5
|
+
*/
|
|
3
6
|
export declare function createRequestHandler(config: Config): (req: Request) => Promise<{
|
|
4
7
|
response: Response;
|
|
5
8
|
headers: {
|
|
6
9
|
[key: string]: string | string[];
|
|
7
10
|
};
|
|
8
11
|
}>;
|
|
12
|
+
/**
|
|
13
|
+
* @deprecated
|
|
14
|
+
*/
|
|
9
15
|
export declare function Resolve(request: Request, tree: RouteTree, config: Config): Promise<{
|
|
10
16
|
response: Response;
|
|
11
17
|
headers: {
|
|
@@ -2,12 +2,18 @@ import { ServerOnlyWarning } from "../util.js";
|
|
|
2
2
|
ServerOnlyWarning("native-request");
|
|
3
3
|
import { GenericContext } from "../router.js";
|
|
4
4
|
import { MakeStatus } from "../../status.js";
|
|
5
|
+
/**
|
|
6
|
+
* @deprecated
|
|
7
|
+
*/
|
|
5
8
|
export function createRequestHandler(config) {
|
|
6
9
|
return async (req) => {
|
|
7
10
|
const mod = typeof config.build === "function" ? await config.build() : await config.build;
|
|
8
11
|
return await Resolve(req, mod.tree, config);
|
|
9
12
|
};
|
|
10
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* @deprecated
|
|
16
|
+
*/
|
|
11
17
|
export async function Resolve(request, tree, config) {
|
|
12
18
|
const ctx = new GenericContext(request, new URL(request.url), config.render);
|
|
13
19
|
let response;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ViteDevServer } from "vite";
|
|
2
|
+
import { connectToWeb } from "./compatibility/vite/connectToWeb.js";
|
|
3
|
+
import { GenericContext } from "../router.js";
|
|
4
|
+
import { RouterModule } from "./index.js";
|
|
5
|
+
export type Config = {
|
|
6
|
+
build: () => Promise<RouterModule> | Promise<RouterModule>;
|
|
7
|
+
render: GenericContext["render"];
|
|
8
|
+
viteDevServer: ViteDevServer | null;
|
|
9
|
+
};
|
|
10
|
+
type ServerBindType = "pre" | "post";
|
|
11
|
+
type ServerBind = (ctx: GenericContext) => Promise<Response | null> | Response | null;
|
|
12
|
+
export declare class HtmxRouterServer {
|
|
13
|
+
#private;
|
|
14
|
+
readonly vite: {
|
|
15
|
+
handler: ReturnType<typeof connectToWeb>;
|
|
16
|
+
server: Config["viteDevServer"];
|
|
17
|
+
} | null;
|
|
18
|
+
readonly render: Config["render"];
|
|
19
|
+
readonly build: Config["build"];
|
|
20
|
+
constructor(config: Config);
|
|
21
|
+
/**
|
|
22
|
+
* Add a middleware to resolve requests before/after the route tree attempts to resolve
|
|
23
|
+
*/
|
|
24
|
+
use(type: ServerBindType, binding: ServerBind): void;
|
|
25
|
+
resolve<T extends boolean>(request: Request, resolve404?: T): Promise<T extends true ? Response : (Response | null)>;
|
|
26
|
+
/**
|
|
27
|
+
* Use the top level error handler in the route tree to render an error
|
|
28
|
+
* @param ctx The context the error came from
|
|
29
|
+
* @param e The error that caused it (if undefined, will be a 404 response)
|
|
30
|
+
*/
|
|
31
|
+
error(ctx: Request | GenericContext, e: unknown | undefined): Promise<Response>;
|
|
32
|
+
/**
|
|
33
|
+
* Create a closure for use with the classic express.js like servers
|
|
34
|
+
*/
|
|
35
|
+
nodeAdaptor(resolve404?: boolean): (req: import("http").IncomingMessage, res: import("http").ServerResponse) => Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { ServerOnlyWarning } from "../util.js";
|
|
2
|
+
ServerOnlyWarning("request/server");
|
|
3
|
+
import { connectToWeb } from "./compatibility/vite/connectToWeb.js";
|
|
4
|
+
import { GenericContext } from "../router.js";
|
|
5
|
+
import { NodeAdaptor } from "./compatibility/node.js";
|
|
6
|
+
import { MakeStatus } from "../../status.js";
|
|
7
|
+
function UrlCleaner({ url }) {
|
|
8
|
+
const i = url.pathname.lastIndexOf("/");
|
|
9
|
+
if (i === 0)
|
|
10
|
+
return null;
|
|
11
|
+
if (i !== url.pathname.length - 1)
|
|
12
|
+
return null;
|
|
13
|
+
url.pathname = url.pathname.slice(0, -1);
|
|
14
|
+
return new Response("", MakeStatus("Permanent Redirect", {
|
|
15
|
+
headers: { location: url.toString() }
|
|
16
|
+
}));
|
|
17
|
+
}
|
|
18
|
+
export class HtmxRouterServer {
|
|
19
|
+
vite;
|
|
20
|
+
render;
|
|
21
|
+
build;
|
|
22
|
+
#binding;
|
|
23
|
+
constructor(config) {
|
|
24
|
+
this.vite = config.viteDevServer ? {
|
|
25
|
+
handler: connectToWeb(config.viteDevServer.middlewares),
|
|
26
|
+
server: config.viteDevServer
|
|
27
|
+
} : null;
|
|
28
|
+
this.render = config.render;
|
|
29
|
+
this.build = config.build;
|
|
30
|
+
this.#binding = {
|
|
31
|
+
pre: [UrlCleaner],
|
|
32
|
+
post: []
|
|
33
|
+
};
|
|
34
|
+
if (config.viteDevServer) {
|
|
35
|
+
const handler = connectToWeb(config.viteDevServer.middlewares);
|
|
36
|
+
this.#binding.post.push(handler);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Add a middleware to resolve requests before/after the route tree attempts to resolve
|
|
41
|
+
*/
|
|
42
|
+
use(type, binding) {
|
|
43
|
+
if (type === "pre") {
|
|
44
|
+
this.#binding.pre.push(binding);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (type === "post") {
|
|
48
|
+
this.#binding.post.push(binding);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
throw new Error(`Unknown binding type "${type}"`);
|
|
52
|
+
}
|
|
53
|
+
async resolve(request, resolve404 = true) {
|
|
54
|
+
const url = new URL(request.url);
|
|
55
|
+
const ctx = new GenericContext(request, url, this.render);
|
|
56
|
+
{ // pre-binding
|
|
57
|
+
const res = await this.#applyBindings("pre", ctx);
|
|
58
|
+
if (res)
|
|
59
|
+
return res;
|
|
60
|
+
}
|
|
61
|
+
const tree = await this.#getTree();
|
|
62
|
+
{ // route
|
|
63
|
+
const res = await this.#resolveRoute(ctx, tree);
|
|
64
|
+
if (res)
|
|
65
|
+
return res;
|
|
66
|
+
}
|
|
67
|
+
{ // post-binding
|
|
68
|
+
const res = await this.#applyBindings("post", ctx);
|
|
69
|
+
if (res)
|
|
70
|
+
return res;
|
|
71
|
+
}
|
|
72
|
+
if (resolve404)
|
|
73
|
+
return await this.error(ctx, undefined);
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Use the top level error handler in the route tree to render an error
|
|
78
|
+
* @param ctx The context the error came from
|
|
79
|
+
* @param e The error that caused it (if undefined, will be a 404 response)
|
|
80
|
+
*/
|
|
81
|
+
async error(ctx, e) {
|
|
82
|
+
if (e === undefined)
|
|
83
|
+
e = new Response("No Route", MakeStatus("Not Found"));
|
|
84
|
+
if (ctx instanceof Request)
|
|
85
|
+
ctx = new GenericContext(ctx, new URL(ctx.url), this.render);
|
|
86
|
+
const tree = await this.#getTree();
|
|
87
|
+
return await tree.unwrap(ctx, e);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Create a closure for use with the classic express.js like servers
|
|
91
|
+
*/
|
|
92
|
+
nodeAdaptor(resolve404 = true) {
|
|
93
|
+
return NodeAdaptor(this, resolve404);
|
|
94
|
+
}
|
|
95
|
+
async #getTree() {
|
|
96
|
+
const mod = typeof this.build === "function" ? await this.build() : await this.build;
|
|
97
|
+
return mod.tree;
|
|
98
|
+
}
|
|
99
|
+
async #resolveRoute(ctx, tree) {
|
|
100
|
+
let response;
|
|
101
|
+
try {
|
|
102
|
+
const x = ctx.url.pathname.slice(1);
|
|
103
|
+
const fragments = x === "" ? [] : x.split("/");
|
|
104
|
+
const res = await tree.resolve(fragments, ctx);
|
|
105
|
+
if (res === null)
|
|
106
|
+
return null;
|
|
107
|
+
response = res;
|
|
108
|
+
}
|
|
109
|
+
catch (e) {
|
|
110
|
+
if (e instanceof Error)
|
|
111
|
+
this.vite?.server?.ssrFixStacktrace(e);
|
|
112
|
+
console.error(e);
|
|
113
|
+
response = await tree.unwrap(ctx, e);
|
|
114
|
+
}
|
|
115
|
+
// context merge headers if divergent
|
|
116
|
+
if (response.headers !== ctx.headers) {
|
|
117
|
+
for (const [key, value] of ctx.headers) {
|
|
118
|
+
if (key === "content-type")
|
|
119
|
+
continue;
|
|
120
|
+
response.headers.set(key, value);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// merge in cookie changes
|
|
124
|
+
const cookies = ctx.cookie.export();
|
|
125
|
+
for (const cookie of cookies)
|
|
126
|
+
response.headers.append("Set-Cookie", cookie);
|
|
127
|
+
return response;
|
|
128
|
+
}
|
|
129
|
+
async #applyBindings(type, ctx) {
|
|
130
|
+
const set = this.#binding[type];
|
|
131
|
+
if (!set)
|
|
132
|
+
throw new Error(`Unknown binding type "${type}"`);
|
|
133
|
+
try {
|
|
134
|
+
for (const bind of set) {
|
|
135
|
+
const res = bind(ctx);
|
|
136
|
+
if (res)
|
|
137
|
+
return res;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
return this.error(ctx.request, e);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
package/package.json
CHANGED
package/router.d.ts
CHANGED
package/server.d.ts
ADDED
package/server.js
ADDED
package/status.d.ts
CHANGED
|
@@ -67,8 +67,4 @@ type Definitions = typeof dictionary;
|
|
|
67
67
|
export type StatusCode = keyof Definitions;
|
|
68
68
|
export type StatusText = Definitions[StatusCode];
|
|
69
69
|
export declare function MakeStatus(lookup: StatusCode | StatusText, init?: ResponseInit | Headers): ResponseInit;
|
|
70
|
-
/**
|
|
71
|
-
* @deprecated
|
|
72
|
-
*/
|
|
73
|
-
export type Status = StatusCode;
|
|
74
70
|
export {};
|
package/status.js
CHANGED