htmx-router 0.2.0 → 1.0.0-alpha.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.
Files changed (55) hide show
  1. package/bin/cli/config.d.ts +10 -0
  2. package/bin/cli/config.js +4 -0
  3. package/bin/cli/index.js +54 -10
  4. package/bin/client/index.d.ts +4 -0
  5. package/bin/client/index.js +100 -0
  6. package/bin/client/mount.d.ts +2 -0
  7. package/bin/client/mount.js +74 -0
  8. package/bin/client/watch.d.ts +1 -0
  9. package/bin/client/watch.js +19 -0
  10. package/bin/helper.d.ts +1 -2
  11. package/bin/helper.js +25 -14
  12. package/bin/index.d.ts +8 -6
  13. package/bin/index.js +7 -16
  14. package/bin/request/http.d.ts +10 -0
  15. package/bin/request/http.js +46 -0
  16. package/bin/request/index.d.ts +16 -0
  17. package/bin/request/index.js +6 -0
  18. package/bin/request/native.d.ts +9 -0
  19. package/bin/request/native.js +56 -0
  20. package/bin/router.d.ts +41 -16
  21. package/bin/router.js +176 -236
  22. package/bin/types.d.ts +10 -0
  23. package/bin/types.js +1 -0
  24. package/bin/util/cookies.d.ts +22 -0
  25. package/bin/util/cookies.js +57 -0
  26. package/bin/util/css.d.ts +9 -0
  27. package/bin/util/css.js +47 -0
  28. package/bin/util/dynamic.d.ts +5 -0
  29. package/bin/util/dynamic.js +26 -0
  30. package/bin/util/endpoint.d.ts +9 -0
  31. package/bin/util/endpoint.js +28 -0
  32. package/bin/util/event-source.d.ts +16 -0
  33. package/bin/util/event-source.js +85 -0
  34. package/bin/util/hash.d.ts +1 -0
  35. package/bin/util/hash.js +7 -0
  36. package/bin/util/index.d.ts +1 -0
  37. package/bin/util/index.js +7 -0
  38. package/bin/util/parameters.d.ts +7 -0
  39. package/bin/util/parameters.js +14 -0
  40. package/bin/util/shell.d.ts +32 -0
  41. package/bin/util/shell.js +1 -0
  42. package/package.json +9 -7
  43. package/readme.md +149 -213
  44. package/bin/404-route.d.ts +0 -2
  45. package/bin/404-route.js +0 -8
  46. package/bin/cli/dynamic.d.ts +0 -2
  47. package/bin/cli/dynamic.js +0 -47
  48. package/bin/cli/static.d.ts +0 -2
  49. package/bin/cli/static.js +0 -49
  50. package/bin/components.d.ts +0 -8
  51. package/bin/components.js +0 -11
  52. package/bin/render-args.d.ts +0 -35
  53. package/bin/render-args.js +0 -120
  54. package/bin/shared.d.ts +0 -28
  55. package/bin/shared.js +0 -28
@@ -0,0 +1,10 @@
1
+ export declare function ReadConfig(): Promise<{
2
+ client?: {
3
+ adapter: string;
4
+ source: string;
5
+ };
6
+ router: {
7
+ folder: string;
8
+ output: string;
9
+ };
10
+ }>;
@@ -0,0 +1,4 @@
1
+ import { readFile } from "fs/promises";
2
+ export async function ReadConfig() {
3
+ return JSON.parse(await readFile(process.argv[2] || "./htmx-router.json", "utf-8"));
4
+ }
package/bin/cli/index.js CHANGED
@@ -1,14 +1,58 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
- Object.defineProperty(exports, "__esModule", { value: true });
4
- const dynamic_1 = require("./dynamic");
5
- const static_1 = require("./static");
6
- const isDynamic = process.argv.includes('--dynamic');
7
- const cwd = process.argv[2] || "./";
8
- console.log(`Building ${isDynamic ? "dynamic" : "static"} routes`);
9
- if (isDynamic) {
10
- (0, dynamic_1.BuildDynamic)(cwd);
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.info("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
- else {
13
- (0, static_1.BuildStatic)(cwd);
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.info("Building client islands");
57
+ await GenerateClient(config.client, true);
14
58
  }
@@ -0,0 +1,4 @@
1
+ export declare function GenerateClient(config: {
2
+ adapter: string;
3
+ source: string;
4
+ }, force?: boolean): Promise<void>;
@@ -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,2 @@
1
+ export declare function _resolve(fragments: string[]): Response | null;
2
+ export declare function GetMountUrl(): string;
@@ -0,0 +1,74 @@
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.info("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
+ export function _resolve(fragments) {
62
+ if (!fragments[2])
63
+ return null;
64
+ // const build = GetSheet();
65
+ if (!fragments[2].startsWith(hash))
66
+ return null;
67
+ const headers = new Headers();
68
+ headers.set("Content-Type", "text/javascript");
69
+ headers.set("Cache-Control", "public, max-age=604800");
70
+ return new Response(script, { headers });
71
+ }
72
+ export function GetMountUrl() {
73
+ return `/_/mount/${hash}.js`;
74
+ }
@@ -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.info("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
- import type * as CSS from 'csstype';
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
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.StyleCSS = void 0;
4
- function StyleCSS(props) {
5
- let out = "";
6
- for (const key in props) {
7
- const value = props[key];
8
- if (typeof (value) !== "string" && typeof (value) !== "number")
9
- continue;
10
- const safeKey = key.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
11
- const safeVal = value.toString().replace(/"/g, "\\\"");
12
- out += `${safeKey}: ${safeVal};`;
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
- return out;
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 { ErrorResponse, Redirect, Outlet, Override } from "./shared";
2
- import { RouteTree, IsAllowedExt } from "./router";
3
- import { RenderArgs } from "./render-args";
4
- import { Link } from "./components";
5
- import { StyleCSS } from "./helper";
6
- export { IsAllowedExt, RouteTree, ErrorResponse, Redirect, Override, RenderArgs, Outlet, StyleCSS, Link };
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
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Link = exports.StyleCSS = exports.RenderArgs = exports.Override = exports.Redirect = exports.ErrorResponse = exports.RouteTree = exports.IsAllowedExt = void 0;
4
- const shared_1 = require("./shared");
5
- Object.defineProperty(exports, "ErrorResponse", { enumerable: true, get: function () { return shared_1.ErrorResponse; } });
6
- Object.defineProperty(exports, "Redirect", { enumerable: true, get: function () { return shared_1.Redirect; } });
7
- Object.defineProperty(exports, "Override", { enumerable: true, get: function () { return shared_1.Override; } });
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,6 @@
1
+ import * as native from "../request/native.js";
2
+ import * as http from "../request/http.js";
3
+ export const createRequestHandler = {
4
+ http: http.createRequestHandler,
5
+ native: native.createRequestHandler
6
+ };
@@ -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
- /// <reference types="node" />
2
- import type http from "node:http";
3
- import { Override, Redirect, RouteModule } from "./shared";
4
- import { MaskType, RenderArgs } from "./render-args";
5
- export declare function IsAllowedExt(ext: string): boolean;
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
- mask: boolean[];
9
- constructor(module: RouteModule, mask: boolean[]);
10
- render(args: RenderArgs, mask: MaskType, routeName: string): Promise<string>;
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
- default: RouteLeaf | null;
17
- route: RouteLeaf | null;
18
- constructor();
19
- assignRoot(module: RouteModule): void;
20
- ingest(path: string | string[], module: RouteModule, override: boolean[]): void;
21
- calculateDepth(from: string[], to: string[]): number;
22
- render(req: http.IncomingMessage, res: http.ServerResponse, url: URL): Promise<string | Redirect | Override>;
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
  }