htmx-router 0.2.0 → 1.0.0-alpha.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/bin/cli/config.d.ts +10 -0
- package/bin/cli/config.js +4 -0
- package/bin/cli/index.js +54 -10
- package/bin/client/index.d.ts +4 -0
- package/bin/client/index.js +100 -0
- package/bin/client/mount.d.ts +2 -0
- package/bin/client/mount.js +75 -0
- package/bin/client/watch.d.ts +1 -0
- package/bin/client/watch.js +19 -0
- package/bin/helper.d.ts +1 -2
- package/bin/helper.js +25 -14
- package/bin/index.d.ts +8 -6
- package/bin/index.js +7 -16
- package/bin/request/http.d.ts +10 -0
- package/bin/request/http.js +46 -0
- package/bin/request/index.d.ts +16 -0
- package/bin/request/index.js +6 -0
- package/bin/request/native.d.ts +9 -0
- package/bin/request/native.js +56 -0
- package/bin/router.d.ts +41 -16
- package/bin/router.js +176 -236
- package/bin/types.d.ts +10 -0
- package/bin/types.js +1 -0
- package/bin/util/cookies.d.ts +22 -0
- package/bin/util/cookies.js +57 -0
- package/bin/util/css.d.ts +9 -0
- package/bin/util/css.js +47 -0
- package/bin/util/dynamic.d.ts +5 -0
- package/bin/util/dynamic.js +26 -0
- package/bin/util/endpoint.d.ts +9 -0
- package/bin/util/endpoint.js +28 -0
- package/bin/util/event-source.d.ts +16 -0
- package/bin/util/event-source.js +85 -0
- package/bin/util/hash.d.ts +1 -0
- package/bin/util/hash.js +7 -0
- package/bin/util/index.d.ts +1 -0
- package/bin/util/index.js +7 -0
- package/bin/util/parameters.d.ts +7 -0
- package/bin/util/parameters.js +14 -0
- package/bin/util/shell.d.ts +32 -0
- package/bin/util/shell.js +1 -0
- package/package.json +9 -7
- package/readme.md +149 -213
- package/bin/404-route.d.ts +0 -2
- package/bin/404-route.js +0 -8
- package/bin/cli/dynamic.d.ts +0 -2
- package/bin/cli/dynamic.js +0 -47
- package/bin/cli/static.d.ts +0 -2
- package/bin/cli/static.js +0 -49
- package/bin/components.d.ts +0 -8
- package/bin/components.js +0 -11
- package/bin/render-args.d.ts +0 -35
- package/bin/render-args.js +0 -120
- package/bin/shared.d.ts +0 -28
- package/bin/shared.js +0 -28
package/bin/cli/index.js
CHANGED
|
@@ -1,14 +1,58 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
console.log(
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
import { writeFile } from "fs/promises";
|
|
4
|
+
import { relative } from "path";
|
|
5
|
+
import { GenerateClient } from "../client/index.js";
|
|
6
|
+
import { ReadConfig } from "../cli/config.js";
|
|
7
|
+
const config = await ReadConfig();
|
|
8
|
+
console.log("Building router");
|
|
9
|
+
const routes = relative(config.router.output, config.router.folder).replaceAll("\\", "/").slice(1);
|
|
10
|
+
await writeFile(config.router.output, `/*------------------------------------------
|
|
11
|
+
* Generated by htmx-router *
|
|
12
|
+
* Warn: Any changes will be overwritten *
|
|
13
|
+
-------------------------------------------*/
|
|
14
|
+
|
|
15
|
+
import { GenericContext, RouteTree } from "htmx-router/bin/router";
|
|
16
|
+
import { RegisterDynamic } from "htmx-router/bin/util/dynamic";
|
|
17
|
+
import { GetMountUrl } from 'htmx-router/bin/client/mount';
|
|
18
|
+
import { GetSheetUrl } from 'htmx-router/bin/util/css';
|
|
19
|
+
import { RouteModule } from "htmx-router";
|
|
20
|
+
|
|
21
|
+
const modules = import.meta.glob('${routes}/**/*.{ts,tsx}', { eager: true });
|
|
22
|
+
|
|
23
|
+
export const tree = new RouteTree();
|
|
24
|
+
for (const path in modules) {
|
|
25
|
+
const tail = path.lastIndexOf(".");
|
|
26
|
+
const url = path.slice(${routes.length + 1}, tail);
|
|
27
|
+
tree.ingest(url, modules[path] as RouteModule<any>);
|
|
11
28
|
}
|
|
12
|
-
|
|
13
|
-
|
|
29
|
+
|
|
30
|
+
export function Dynamic<T extends Record<string, string>>(props: {
|
|
31
|
+
params: T,
|
|
32
|
+
loader: (params: T, ctx: GenericContext) => Promise<JSX.Element>
|
|
33
|
+
children?: JSX.Element
|
|
34
|
+
}): JSX.Element {
|
|
35
|
+
const path = RegisterDynamic(props.loader);
|
|
36
|
+
|
|
37
|
+
const query = new URLSearchParams();
|
|
38
|
+
for (const key in props.params) query.set(key, props.params[key]);
|
|
39
|
+
const url = path + query.toString();
|
|
40
|
+
|
|
41
|
+
return <div
|
|
42
|
+
hx-get={url}
|
|
43
|
+
hx-trigger="load"
|
|
44
|
+
hx-swap="outerHTML transition:true"
|
|
45
|
+
style={{ display: "contents" }}
|
|
46
|
+
>{props.children ? props.children : ""}</div>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function RouteHeaders() {
|
|
50
|
+
return <>
|
|
51
|
+
<link href={GetSheetUrl()} rel="stylesheet"></link>
|
|
52
|
+
<script src={GetMountUrl()}></script>
|
|
53
|
+
</>
|
|
54
|
+
}`);
|
|
55
|
+
if (config.client) {
|
|
56
|
+
console.log("Building client islands");
|
|
57
|
+
await GenerateClient(config.client, true);
|
|
14
58
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { readFile, writeFile } from "fs/promises";
|
|
2
|
+
import { init, parse } from "es-module-lexer";
|
|
3
|
+
import { QuickHash } from "../util/hash.js";
|
|
4
|
+
import { CutString } from "../helper.js";
|
|
5
|
+
const pivot = `\n// DO NOT EDIT BELOW THIS LINE\n`;
|
|
6
|
+
export async function GenerateClient(config, force = false) {
|
|
7
|
+
const file = await readFile(config.source, "utf8");
|
|
8
|
+
const [source, history] = CutString(file, pivot);
|
|
9
|
+
const hash = QuickHash(source);
|
|
10
|
+
if (!force && ExtractHash(history) === hash)
|
|
11
|
+
return;
|
|
12
|
+
await init;
|
|
13
|
+
const parsed = parse(source)[0];
|
|
14
|
+
const imports = new Array();
|
|
15
|
+
const names = new Array();
|
|
16
|
+
for (const imp of parsed) {
|
|
17
|
+
if (imp.a !== -1)
|
|
18
|
+
continue;
|
|
19
|
+
if (imp.t !== 1)
|
|
20
|
+
continue;
|
|
21
|
+
imports.push(source.slice(imp.ss, imp.se));
|
|
22
|
+
names.push(...ExtractNames(source.slice(imp.ss, imp.s)));
|
|
23
|
+
}
|
|
24
|
+
await writeFile(config.source, source
|
|
25
|
+
+ pivot
|
|
26
|
+
+ `// hash: ${hash}\n`
|
|
27
|
+
+ BuildClientServer(names));
|
|
28
|
+
await writeFile(CutString(config.source, ".", -1)[0] + ".manifest.tsx", BuildClientManifest(config.adapter, names, imports));
|
|
29
|
+
}
|
|
30
|
+
function BuildClientServer(names) {
|
|
31
|
+
let out = "type FirstArg<T> = T extends (arg: infer U, ...args: any[]) => any ? U : never;\n"
|
|
32
|
+
+ "function mount(name: string, data: string, ssr?: JSX.Element) {\n"
|
|
33
|
+
+ "\treturn (<>\n"
|
|
34
|
+
+ `\t\t<div style={{ display: "contents" }}>{ssr}</div>\n`
|
|
35
|
+
+ "\t\t<script>{`Router.mountAboveWith(\"${name}\", JSON.parse(\"${data}\"))`}</script>\n"
|
|
36
|
+
+ "\t</>);\n"
|
|
37
|
+
+ "}\n"
|
|
38
|
+
+ "\n"
|
|
39
|
+
+ "const Client = {\n";
|
|
40
|
+
for (const name of names) {
|
|
41
|
+
out += `\t${name}: function(props: FirstArg<typeof ${name}> & { children?: JSX.Element }) {\n`
|
|
42
|
+
+ `\t\tconst { children, ...rest } = props;\n`
|
|
43
|
+
+ `\t\treturn mount("${name}", JSON.stringify(rest), children);\n`
|
|
44
|
+
+ `\t},\n`;
|
|
45
|
+
}
|
|
46
|
+
out += "}\nexport default Client;\n\n"
|
|
47
|
+
+ `if (process.env.NODE_ENV !== "production") {\n`
|
|
48
|
+
+ `\t(await import( "htmx-router/bin/client/watch.js")).WatchClient();\n`
|
|
49
|
+
+ `}`;
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
function BuildClientManifest(type, names, imports) {
|
|
53
|
+
let out = "/*------------------------------------------\n"
|
|
54
|
+
+ " * Generated by htmx-router *\n"
|
|
55
|
+
+ " * Warn: Any changes will be overwritten *\n"
|
|
56
|
+
+ "-------------------------------------------*/\n"
|
|
57
|
+
+ imports.join(";\n") + ";\n";
|
|
58
|
+
switch (type) {
|
|
59
|
+
case "react":
|
|
60
|
+
out += BuildReactClientManifest(names);
|
|
61
|
+
break;
|
|
62
|
+
default:
|
|
63
|
+
console.error(`Unsupported client adapter ${type}`);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
out += "export default client;\n"
|
|
67
|
+
+ "(window as any).CLIENT = client;";
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
function BuildReactClientManifest(names) {
|
|
71
|
+
let out = `import ReactDOM from "react-dom/client";\n\n`
|
|
72
|
+
+ "const client = {\n";
|
|
73
|
+
for (const name of names)
|
|
74
|
+
out += `\t${name}: (element: HTMLElement, props: any) => ReactDOM.createRoot(element).render(<${name} {...props} />),\n`;
|
|
75
|
+
out += "};\n";
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
function ExtractNames(str) {
|
|
79
|
+
const start = str.indexOf("{");
|
|
80
|
+
if (start === -1) {
|
|
81
|
+
const middle = CutString(CutString(str, "import")[1], "from", -1)[0];
|
|
82
|
+
return [ExtractName(middle)];
|
|
83
|
+
}
|
|
84
|
+
const end = str.lastIndexOf("}");
|
|
85
|
+
const segments = str.slice(start + 1, end).split(",");
|
|
86
|
+
return segments.map(ExtractName);
|
|
87
|
+
}
|
|
88
|
+
function ExtractName(str) {
|
|
89
|
+
const parts = CutString(str, "as");
|
|
90
|
+
if (parts[1])
|
|
91
|
+
return parts[1].trim();
|
|
92
|
+
return parts[0].trim();
|
|
93
|
+
}
|
|
94
|
+
function ExtractHash(source) {
|
|
95
|
+
const regex = /\/\/\s+hash\s*:\s*(\w+)/;
|
|
96
|
+
const match = source.match(regex);
|
|
97
|
+
if (match)
|
|
98
|
+
return match[1] || "";
|
|
99
|
+
return "";
|
|
100
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { QuickHash } from "../util/hash.js";
|
|
2
|
+
import { CutString } from "../helper.js";
|
|
3
|
+
function ClientMounter() {
|
|
4
|
+
const theme = {
|
|
5
|
+
infer: () => {
|
|
6
|
+
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
7
|
+
const current = prefersDark ? 'dark' : 'light';
|
|
8
|
+
localStorage.setItem("theme", current);
|
|
9
|
+
return current;
|
|
10
|
+
},
|
|
11
|
+
apply: () => {
|
|
12
|
+
const current = localStorage.getItem("theme") || theme.infer();
|
|
13
|
+
document.documentElement.setAttribute('data-theme', current);
|
|
14
|
+
},
|
|
15
|
+
toggle: () => {
|
|
16
|
+
const current = localStorage.getItem("theme") || theme.infer();
|
|
17
|
+
if (current === "dark")
|
|
18
|
+
localStorage.setItem("theme", "light");
|
|
19
|
+
else
|
|
20
|
+
localStorage.setItem("theme", "dark");
|
|
21
|
+
theme.apply();
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
|
25
|
+
theme.infer();
|
|
26
|
+
theme.apply();
|
|
27
|
+
});
|
|
28
|
+
theme.apply();
|
|
29
|
+
const global = window;
|
|
30
|
+
const mountRequests = new Array();
|
|
31
|
+
function RequestMount(funcName, json) {
|
|
32
|
+
mountRequests.push([funcName, document.currentScript.previousElementSibling, json]);
|
|
33
|
+
}
|
|
34
|
+
function Mount() {
|
|
35
|
+
if (mountRequests.length < 1)
|
|
36
|
+
return;
|
|
37
|
+
if (!global.CLIENT)
|
|
38
|
+
throw new Error("Client manifest missing");
|
|
39
|
+
console.log("hydrating...");
|
|
40
|
+
for (const [funcName, element, json] of mountRequests) {
|
|
41
|
+
const func = global.CLIENT[funcName];
|
|
42
|
+
if (!func)
|
|
43
|
+
throw new Error(`Component ${funcName} is missing from client manifest`);
|
|
44
|
+
func(element, json);
|
|
45
|
+
}
|
|
46
|
+
mountRequests.length = 0;
|
|
47
|
+
}
|
|
48
|
+
document.addEventListener("DOMContentLoaded", Mount);
|
|
49
|
+
if (global.htmx)
|
|
50
|
+
global.htmx.onLoad(Mount);
|
|
51
|
+
return {
|
|
52
|
+
mountAboveWith: RequestMount,
|
|
53
|
+
theme
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
;
|
|
57
|
+
const script = "window.Router = (function () {"
|
|
58
|
+
+ CutString(ClientMounter.toString(), "{")[1]
|
|
59
|
+
+ ")();";
|
|
60
|
+
const hash = QuickHash(script);
|
|
61
|
+
console.log(CutString(ClientMounter.toString(), "{")[1]);
|
|
62
|
+
export function _resolve(fragments) {
|
|
63
|
+
if (!fragments[2])
|
|
64
|
+
return null;
|
|
65
|
+
// const build = GetSheet();
|
|
66
|
+
if (!fragments[2].startsWith(hash))
|
|
67
|
+
return null;
|
|
68
|
+
const headers = new Headers();
|
|
69
|
+
headers.set("Content-Type", "text/javascript");
|
|
70
|
+
headers.set("Cache-Control", "public, max-age=604800");
|
|
71
|
+
return new Response(script, { headers });
|
|
72
|
+
}
|
|
73
|
+
export function GetMountUrl() {
|
|
74
|
+
return `/_/mount/${hash}.js`;
|
|
75
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function WatchClient(): Promise<void>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { watch } from "fs";
|
|
2
|
+
import { GenerateClient } from "../client/index.js";
|
|
3
|
+
import { ReadConfig } from "../cli/config.js";
|
|
4
|
+
export async function WatchClient() {
|
|
5
|
+
if (process.env.NODE_ENV === "production") {
|
|
6
|
+
console.warn("Watching client islands is disabled in production");
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
const config = await ReadConfig();
|
|
10
|
+
const client = config.client;
|
|
11
|
+
if (!client)
|
|
12
|
+
return;
|
|
13
|
+
const rebuild = () => {
|
|
14
|
+
console.log("Building client");
|
|
15
|
+
GenerateClient(client).catch(console.error);
|
|
16
|
+
};
|
|
17
|
+
watch(client.source, rebuild);
|
|
18
|
+
rebuild();
|
|
19
|
+
}
|
package/bin/helper.d.ts
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export declare function StyleCSS(props: CSS.Properties<string | number>): string;
|
|
1
|
+
export declare function CutString(str: string, pivot: string, offset?: number): [string, string];
|
package/bin/helper.js
CHANGED
|
@@ -1,16 +1,27 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
export function CutString(str, pivot, offset = 1) {
|
|
2
|
+
if (offset > 0) {
|
|
3
|
+
let cursor = 0;
|
|
4
|
+
while (offset !== 0) {
|
|
5
|
+
const i = str.indexOf(pivot, cursor);
|
|
6
|
+
if (i === -1)
|
|
7
|
+
return [str, ""];
|
|
8
|
+
cursor = i + 1;
|
|
9
|
+
offset--;
|
|
10
|
+
}
|
|
11
|
+
cursor--;
|
|
12
|
+
return [str.slice(0, cursor), str.slice(cursor + pivot.length)];
|
|
13
13
|
}
|
|
14
|
-
|
|
14
|
+
if (offset < 0) {
|
|
15
|
+
let cursor = str.length;
|
|
16
|
+
while (offset !== 0) {
|
|
17
|
+
const i = str.lastIndexOf(pivot, cursor);
|
|
18
|
+
if (i === -1)
|
|
19
|
+
return [str, ""];
|
|
20
|
+
cursor = i - 1;
|
|
21
|
+
offset++;
|
|
22
|
+
}
|
|
23
|
+
cursor++;
|
|
24
|
+
return [str.slice(0, cursor), str.slice(cursor + pivot.length)];
|
|
25
|
+
}
|
|
26
|
+
return [str, ""];
|
|
15
27
|
}
|
|
16
|
-
exports.StyleCSS = StyleCSS;
|
package/bin/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
1
|
+
import { RouteModule, CatchFunction, RenderFunction } from './types.js';
|
|
2
|
+
import { RouteContext, GenericContext } from "./router.js";
|
|
3
|
+
import { createRequestHandler } from './request/index.js';
|
|
4
|
+
import { Cookies, CookieOptions } from "./util/cookies.js";
|
|
5
|
+
import { EventSourceConnection } from "./util/event-source.js";
|
|
6
|
+
import { StyleClass } from './util/css.js';
|
|
7
|
+
import { Endpoint } from './util/endpoint.js';
|
|
8
|
+
export { CatchFunction, CookieOptions, Cookies, createRequestHandler, Endpoint, EventSourceConnection, GenericContext, RenderFunction, RouteContext, RouteModule, StyleClass, };
|
package/bin/index.js
CHANGED
|
@@ -1,16 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const router_1 = require("./router");
|
|
9
|
-
Object.defineProperty(exports, "RouteTree", { enumerable: true, get: function () { return router_1.RouteTree; } });
|
|
10
|
-
Object.defineProperty(exports, "IsAllowedExt", { enumerable: true, get: function () { return router_1.IsAllowedExt; } });
|
|
11
|
-
const render_args_1 = require("./render-args");
|
|
12
|
-
Object.defineProperty(exports, "RenderArgs", { enumerable: true, get: function () { return render_args_1.RenderArgs; } });
|
|
13
|
-
const components_1 = require("./components");
|
|
14
|
-
Object.defineProperty(exports, "Link", { enumerable: true, get: function () { return components_1.Link; } });
|
|
15
|
-
const helper_1 = require("./helper");
|
|
16
|
-
Object.defineProperty(exports, "StyleCSS", { enumerable: true, get: function () { return helper_1.StyleCSS; } });
|
|
1
|
+
import { RouteContext, GenericContext } from "./router.js";
|
|
2
|
+
import { createRequestHandler } from './request/index.js';
|
|
3
|
+
import { Cookies } from "./util/cookies.js";
|
|
4
|
+
import { EventSourceConnection } from "./util/event-source.js";
|
|
5
|
+
import { StyleClass } from './util/css.js';
|
|
6
|
+
import { Endpoint } from './util/endpoint.js';
|
|
7
|
+
export { Cookies, createRequestHandler, Endpoint, EventSourceConnection, GenericContext, RouteContext, StyleClass, };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "http";
|
|
2
|
+
import type { ViteDevServer } from "vite";
|
|
3
|
+
import { GenericContext } from "../router.js";
|
|
4
|
+
type Config = {
|
|
5
|
+
build: Promise<any> | (() => Promise<Record<string, any>>);
|
|
6
|
+
viteDevServer: ViteDevServer | null;
|
|
7
|
+
render: GenericContext["render"];
|
|
8
|
+
};
|
|
9
|
+
export declare function createRequestHandler(config: Config): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Resolve } from "../request/native.js";
|
|
2
|
+
export function createRequestHandler(config) {
|
|
3
|
+
return async (req, res) => {
|
|
4
|
+
try {
|
|
5
|
+
const mod = typeof config.build === "function" ? await config.build() : await config.build;
|
|
6
|
+
const request = NativeRequest(req);
|
|
7
|
+
let { response, headers } = await Resolve(request, mod.tree, config);
|
|
8
|
+
res.writeHead(response.status, headers);
|
|
9
|
+
let rendered = await response.text();
|
|
10
|
+
if (config.viteDevServer) {
|
|
11
|
+
if (!headers["x-partial"] && response.headers.get("content-type")?.startsWith("text/html")) {
|
|
12
|
+
rendered = await config.viteDevServer.transformIndexHtml(req.url || "", rendered);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
res.end(rendered);
|
|
16
|
+
}
|
|
17
|
+
catch (e) {
|
|
18
|
+
res.statusCode = 500;
|
|
19
|
+
if (e instanceof Error) {
|
|
20
|
+
console.error(e.stack);
|
|
21
|
+
config.viteDevServer?.ssrFixStacktrace(e);
|
|
22
|
+
res.end(e.stack);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
console.error(e);
|
|
26
|
+
res.end(String(e));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function NativeRequest(req) {
|
|
32
|
+
const ctrl = new AbortController();
|
|
33
|
+
const headers = new Headers(req.headers);
|
|
34
|
+
const url = new URL(`http://${headers.get('host')}${req.originalUrl || req.url}`);
|
|
35
|
+
req.once('aborted', () => ctrl.abort());
|
|
36
|
+
const bodied = req.method !== "GET" && req.method !== "HEAD";
|
|
37
|
+
return new Request(url, {
|
|
38
|
+
headers,
|
|
39
|
+
method: req.method,
|
|
40
|
+
body: bodied ? req : undefined,
|
|
41
|
+
signal: ctrl.signal,
|
|
42
|
+
referrer: headers.get("referrer") || undefined,
|
|
43
|
+
// @ts-ignore
|
|
44
|
+
duplex: bodied ? 'half' : undefined
|
|
45
|
+
});
|
|
46
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ViteDevServer } from "vite";
|
|
2
|
+
import * as native from "../request/native.js";
|
|
3
|
+
import * as http from "../request/http.js";
|
|
4
|
+
import { GenericContext, RouteTree } from '../router.js';
|
|
5
|
+
export type Config = {
|
|
6
|
+
build: Promise<any> | (() => Promise<Record<string, any>>);
|
|
7
|
+
viteDevServer: ViteDevServer | null;
|
|
8
|
+
render: GenericContext["render"];
|
|
9
|
+
};
|
|
10
|
+
export type RouterModule = {
|
|
11
|
+
tree: RouteTree;
|
|
12
|
+
};
|
|
13
|
+
export declare const createRequestHandler: {
|
|
14
|
+
http: typeof http.createRequestHandler;
|
|
15
|
+
native: typeof native.createRequestHandler;
|
|
16
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { RouteTree } from '../router.js';
|
|
2
|
+
import { Config } from '../request/index.js';
|
|
3
|
+
export declare function createRequestHandler(config: Config): (req: Request) => Promise<Response>;
|
|
4
|
+
export declare function Resolve(request: Request, tree: RouteTree, config: Config): Promise<{
|
|
5
|
+
response: Response;
|
|
6
|
+
headers: {
|
|
7
|
+
[key: string]: string | string[];
|
|
8
|
+
};
|
|
9
|
+
}>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { GenericContext } from '../router.js';
|
|
2
|
+
export function createRequestHandler(config) {
|
|
3
|
+
return async (req) => {
|
|
4
|
+
try {
|
|
5
|
+
const mod = typeof config.build === "function" ? await config.build() : await config.build;
|
|
6
|
+
let { response, headers } = await Resolve(req, mod.tree, config);
|
|
7
|
+
if (config.viteDevServer) {
|
|
8
|
+
if (!headers["x-partial"] && response.headers.get("content-type")?.startsWith("text/html")) {
|
|
9
|
+
const rendered = await config.viteDevServer.transformIndexHtml(req.url || "", await response.text());
|
|
10
|
+
return new Response(rendered, {
|
|
11
|
+
status: response.status,
|
|
12
|
+
statusText: response.statusText,
|
|
13
|
+
headers: response.headers,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return response;
|
|
18
|
+
}
|
|
19
|
+
catch (e) {
|
|
20
|
+
if (e instanceof Error) {
|
|
21
|
+
console.error(e.stack);
|
|
22
|
+
config.viteDevServer?.ssrFixStacktrace(e);
|
|
23
|
+
return new Response(e.message + "\n" + e.stack, { status: 500, statusText: "Internal Server Error" });
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
console.error(e);
|
|
27
|
+
return new Response(String(e), { status: 500, statusText: "Internal Server Error" });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export async function Resolve(request, tree, config) {
|
|
33
|
+
const url = new URL(request.url);
|
|
34
|
+
const ctx = new GenericContext(request, url, config.render);
|
|
35
|
+
const x = ctx.url.pathname.endsWith("/") ? ctx.url.pathname.slice(0, -1) : ctx.url.pathname;
|
|
36
|
+
const fragments = x.split("/").slice(1);
|
|
37
|
+
let response = await tree.resolve(fragments, ctx);
|
|
38
|
+
if (response === null)
|
|
39
|
+
response = new Response("Not Found", { status: 404, statusText: "Not Found", headers: ctx.headers });
|
|
40
|
+
// Merge context headers
|
|
41
|
+
if (response.headers !== ctx.headers) {
|
|
42
|
+
for (const [key, value] of ctx.headers) {
|
|
43
|
+
if (response.headers.has(key))
|
|
44
|
+
continue;
|
|
45
|
+
response.headers.set(key, value);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Merge cookie changes
|
|
49
|
+
const headers = Object.fromEntries(ctx.headers);
|
|
50
|
+
const cookies = ctx.cookie.export();
|
|
51
|
+
if (cookies.length > 0) {
|
|
52
|
+
headers['set-cookie'] = cookies;
|
|
53
|
+
response.headers.set("Set-Cookie", cookies[0]); // Response object doesn't support multi-header..[]
|
|
54
|
+
}
|
|
55
|
+
return { response, headers };
|
|
56
|
+
}
|
package/bin/router.d.ts
CHANGED
|
@@ -1,23 +1,48 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { Parameterized, ParameterShaper } from './util/parameters.js';
|
|
2
|
+
import { RouteModule } from "./types.js";
|
|
3
|
+
import { Cookies } from './util/cookies.js';
|
|
4
|
+
export declare class GenericContext {
|
|
5
|
+
request: Request;
|
|
6
|
+
headers: Headers;
|
|
7
|
+
cookie: Cookies;
|
|
8
|
+
params: {
|
|
9
|
+
[key: string]: string;
|
|
10
|
+
};
|
|
11
|
+
url: URL;
|
|
12
|
+
render: (res: JSX.Element) => Response;
|
|
13
|
+
constructor(request: GenericContext["request"], url: GenericContext["url"], renderer: GenericContext["render"]);
|
|
14
|
+
shape<T extends ParameterShaper>(shape: T): RouteContext<T>;
|
|
15
|
+
}
|
|
16
|
+
export declare class RouteContext<T extends ParameterShaper> {
|
|
17
|
+
request: Request;
|
|
18
|
+
headers: Headers;
|
|
19
|
+
cookie: Cookies;
|
|
20
|
+
params: Parameterized<T>;
|
|
21
|
+
url: URL;
|
|
22
|
+
render: (res: JSX.Element) => Response;
|
|
23
|
+
constructor(base: GenericContext, shape: T);
|
|
24
|
+
}
|
|
6
25
|
export declare class RouteLeaf {
|
|
7
|
-
module: RouteModule
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
26
|
+
module: RouteModule<any>;
|
|
27
|
+
constructor(module: RouteModule<any>);
|
|
28
|
+
resolve(ctx: GenericContext): Promise<Response | null>;
|
|
29
|
+
error(ctx: GenericContext, e: unknown): Promise<Response | null>;
|
|
30
|
+
private renderWrapper;
|
|
11
31
|
}
|
|
12
32
|
export declare class RouteTree {
|
|
33
|
+
root: boolean;
|
|
13
34
|
nested: Map<string, RouteTree>;
|
|
35
|
+
index: RouteLeaf | null;
|
|
36
|
+
slug: RouteLeaf | null;
|
|
14
37
|
wild: RouteTree | null;
|
|
15
38
|
wildCard: string;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
39
|
+
constructor(root?: boolean);
|
|
40
|
+
ingest(path: string | string[], module: RouteModule<any>): void;
|
|
41
|
+
resolve(fragments: string[], ctx: GenericContext): Promise<Response | null>;
|
|
42
|
+
private resolveIndex;
|
|
43
|
+
private resolveNext;
|
|
44
|
+
private resolveWild;
|
|
45
|
+
private resolveSlug;
|
|
46
|
+
private resolveNative;
|
|
47
|
+
private unwrap;
|
|
23
48
|
}
|