htmx-router 1.0.3 → 1.0.5

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 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
- const headers = new Headers();
57
- headers.set("Content-Type", "text/css");
58
- headers.set("Cache-Control", "public, max-age=604800");
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();
@@ -5,7 +5,7 @@ import { Deferral } from "htmx-router/defer";
5
5
  export function Defer<T extends ParameterShaper>(props: {
6
6
  params?: Parameterized<T>,
7
7
  loader: RenderFunction<T>,
8
- children?: JSX.Element
8
+ children?: JSX.Element[] | JSX.Element
9
9
  }): JSX.Element {
10
10
  return <div
11
11
  hx-get={Deferral(props.loader, props.params)}
@@ -1,6 +1,6 @@
1
1
  const generic = `import { RenderMetaDescriptor, ShellOptions } from "htmx-router/shell";
2
2
 
3
- export function Head<T>(props: { options: ShellOptions<T>, children: JSX.Element }) {
3
+ export function Head<T>(props: { options: ShellOptions<T>, children: JSX.Element[] | JSX.Element }) {
4
4
  return <head>
5
5
  { RenderMetaDescriptor(props.options) as "safe" }
6
6
  { props.children as "safe" }
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
- const headers = new Headers();
124
- headers.set("Content-Type", "text/javascript");
125
- headers.set("Cache-Control", "public, max-age=604800");
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 type { GenericContext } from "../router.js";
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<Response>;
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,38 @@
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
- try {
7
- const mod = typeof config.build === "function" ? await config.build() : await config.build;
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 url = new URL(request.url);
26
- const ctx = new GenericContext(request, url, config.render);
27
- const x = ctx.url.pathname.endsWith("/") ? ctx.url.pathname.slice(0, -1) : ctx.url.pathname;
28
- const fragments = x.split("/").slice(1);
29
- let response = await tree.resolve(fragments, ctx);
30
- if (response === null)
31
- response = new Response("No Route Found", { status: 404, statusText: "Not Found", headers: ctx.headers });
32
- // Override with context headers
33
- if (response.headers !== ctx.headers) {
34
- for (const [key, value] of ctx.headers) {
35
- if (response.headers.has(key))
36
- continue;
37
- response.headers.set(key, value);
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
+ response.headers.set(key, value);
25
+ }
26
+ }
27
+ catch (e) {
28
+ if (e instanceof Error) {
29
+ console.error(e.stack);
30
+ config.viteDevServer?.ssrFixStacktrace(e);
31
+ response = new Response(e.message + "\n" + e.stack, { status: 500, statusText: "Internal Server Error" });
32
+ }
33
+ else {
34
+ console.error(e);
35
+ response = new Response(String(e), { status: 500, statusText: "Internal Server Error" });
38
36
  }
39
37
  }
40
38
  // Merge cookie changes
@@ -17,6 +17,7 @@ export class GenericContext {
17
17
  this.url = url;
18
18
  this.render = renderer;
19
19
  this.headers.set("x-powered-by", "htmx-router");
20
+ this.headers.set("content-type", "text/html");
20
21
  }
21
22
  shape(shape) {
22
23
  return new RouteContext(this, this.params, shape);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "htmx-router",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "A lightweight SSR framework with server+client islands",
5
5
  "keywords": [
6
6
  "htmx",
package/router.js CHANGED
@@ -132,6 +132,8 @@ export class RouteTree {
132
132
  const res = await this.index.resolve(ctx);
133
133
  if (res instanceof Response)
134
134
  return res;
135
+ if (res === null)
136
+ return null;
135
137
  return new Response(res, { headers: ctx.headers });
136
138
  }
137
139
  async resolveNext(fragments, ctx) {
@@ -154,11 +156,11 @@ export class RouteTree {
154
156
  if (!this.slug)
155
157
  return null;
156
158
  ctx.params["$"] = fragments.join("/");
157
- const res = this.slug.resolve
158
- ? await this.slug.resolve(ctx)
159
- : null;
159
+ const res = await this.slug.resolve(ctx);
160
160
  if (res instanceof Response)
161
161
  return res;
162
+ if (res === null)
163
+ return null;
162
164
  return new Response(res, { headers: ctx.headers });
163
165
  }
164
166
  async unwrap(ctx, res) {
@@ -183,7 +185,7 @@ class RouteLeaf {
183
185
  this.module = module;
184
186
  }
185
187
  async resolve(ctx) {
186
- const res = await this.renderWrapper(ctx);
188
+ const res = await this.response(ctx);
187
189
  if (res === null)
188
190
  return null;
189
191
  if (res instanceof Response)
@@ -198,7 +200,7 @@ class RouteLeaf {
198
200
  return res;
199
201
  return await ctx.render(res, ctx.headers);
200
202
  }
201
- async renderWrapper(ctx) {
203
+ async response(ctx) {
202
204
  try {
203
205
  if (!this.module.loader && !this.module.action)
204
206
  return null;
package/status.d.ts CHANGED
@@ -20,11 +20,13 @@ declare const definitions: {
20
20
  304: "Not Modified";
21
21
  305: "Use Proxy";
22
22
  306: "Switch Proxy";
23
+ 307: "Temporary Redirect";
23
24
  308: "Permanent Redirect";
24
25
  400: "Bad Request";
25
26
  401: "Unauthorized";
26
27
  402: "Payment Required";
27
28
  403: "Forbidden";
29
+ 404: "Not Found";
28
30
  405: "Method Not Allowed";
29
31
  406: "Not Acceptable";
30
32
  407: "Proxy Authentication Required";
@@ -62,8 +64,5 @@ declare const definitions: {
62
64
  511: "Network Authentication Required";
63
65
  };
64
66
  export type StatusText = typeof definitions[keyof typeof definitions];
65
- export declare function MakeStatus(lookup: number | StatusText | string): {
66
- status: number;
67
- statusText: string;
68
- };
67
+ export declare function MakeStatus(lookup: number | StatusText, init?: ResponseInit | Headers): ResponseInit;
69
68
  export {};
package/status.js CHANGED
@@ -20,11 +20,13 @@ const definitions = {
20
20
  304: "Not Modified",
21
21
  305: "Use Proxy",
22
22
  306: "Switch Proxy",
23
+ 307: "Temporary Redirect",
23
24
  308: "Permanent Redirect",
24
25
  400: "Bad Request",
25
26
  401: "Unauthorized",
26
27
  402: "Payment Required",
27
28
  403: "Forbidden",
29
+ 404: "Not Found",
28
30
  405: "Method Not Allowed",
29
31
  406: "Not Acceptable",
30
32
  407: "Proxy Authentication Required",
@@ -71,21 +73,32 @@ for (const key in definitions) {
71
73
  const code = Number(key);
72
74
  index.set(definitions[code].toLowerCase(), code);
73
75
  }
74
- export function MakeStatus(lookup) {
76
+ export function MakeStatus(lookup, init) {
77
+ if (init instanceof Headers)
78
+ init = { headers: init };
75
79
  if (typeof lookup === "number")
76
- return lookupCode(lookup);
77
- return lookupStatus(lookup);
80
+ return lookupCode(lookup, init);
81
+ return lookupStatus(lookup, init);
78
82
  }
79
- function lookupCode(status) {
83
+ function lookupCode(status, init) {
80
84
  if (status < 100)
81
85
  throw new TypeError(`Status ${status}<100`);
82
86
  if (status > 599)
83
87
  throw new TypeError(`Status ${status}>599`);
84
- return { status, statusText: lookup[status] };
88
+ const statusText = lookup[status];
89
+ return Status(status, statusText, init);
85
90
  }
86
- function lookupStatus(statusText) {
91
+ function lookupStatus(statusText, init) {
87
92
  const status = index.get(statusText.toLowerCase());
88
93
  if (!status)
89
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
+ }
90
103
  return { status, statusText };
91
104
  }