htmx-router 1.0.0-alpha.5 → 1.0.0-alpha.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.
Files changed (94) hide show
  1. package/{bin/util/css.js → css.js} +1 -1
  2. package/{bin/util/dynamic.d.ts → dynamic.d.ts} +2 -5
  3. package/{bin/util/dynamic.js → dynamic.js} +7 -5
  4. package/{bin/util/endpoint.d.ts → endpoint.d.ts} +2 -2
  5. package/{bin/util/endpoint.js → endpoint.js} +3 -1
  6. package/event-source.d.ts +26 -0
  7. package/event-source.js +123 -0
  8. package/example/eventdim-react/package.json +67 -0
  9. package/example/eventdim-react/server.js +90 -0
  10. package/example/island-react/global.d.ts +8 -0
  11. package/example/island-react/package.json +38 -0
  12. package/example/island-react/server.js +58 -0
  13. package/global.d.ts +7 -0
  14. package/index.d.ts +19 -0
  15. package/index.js +2 -0
  16. package/internal/cli/config.d.ts +13 -0
  17. package/internal/cli/config.js +11 -0
  18. package/internal/cli/index.js +15 -0
  19. package/internal/client.d.ts +1 -0
  20. package/{bin/client/entry.js → internal/client.js} +3 -1
  21. package/internal/compile/manifest.d.ts +1 -0
  22. package/{bin/client/index.js → internal/compile/manifest.js} +111 -65
  23. package/internal/compile/router.d.ts +1 -0
  24. package/internal/compile/router.js +51 -0
  25. package/internal/component/dynamic.d.ts +4 -0
  26. package/internal/component/dynamic.js +18 -0
  27. package/internal/component/head.d.ts +5 -0
  28. package/internal/component/head.js +22 -0
  29. package/internal/component/scripts.d.ts +4 -0
  30. package/internal/component/scripts.js +23 -0
  31. package/{bin/client → internal}/mount.js +9 -44
  32. package/internal/request/http.d.ts +10 -0
  33. package/internal/request/http.js +61 -0
  34. package/{bin → internal}/request/index.d.ts +3 -3
  35. package/internal/request/index.js +8 -0
  36. package/{bin → internal}/request/native.d.ts +2 -2
  37. package/internal/request/native.js +48 -0
  38. package/{bin/helper.d.ts → internal/util.d.ts} +2 -0
  39. package/{bin/helper.js → internal/util.js} +15 -0
  40. package/package.json +9 -5
  41. package/readme.md +2 -214
  42. package/{bin/request → request}/http.d.ts +1 -1
  43. package/{bin/request → request}/http.js +22 -4
  44. package/request/index.d.ts +13 -0
  45. package/request/index.js +3 -0
  46. package/request/native.d.ts +9 -0
  47. package/{bin/request → request}/native.js +2 -2
  48. package/{bin/util/response.d.ts → response.d.ts} +3 -1
  49. package/{bin/util/response.js → response.js} +5 -5
  50. package/{bin/router.d.ts → router.d.ts} +12 -10
  51. package/{bin/router.js → router.js} +61 -47
  52. package/{bin/util/shell.js → shell.js} +3 -1
  53. package/{bin/util → util}/parameters.d.ts +0 -3
  54. package/{bin/util → util}/parameters.js +0 -3
  55. package/{bin/util → util}/path-builder.js +2 -0
  56. package/util/route.d.ts +2 -0
  57. package/util/route.js +58 -0
  58. package/vite/bundle-splitter.d.ts +4 -0
  59. package/vite/bundle-splitter.js +26 -0
  60. package/vite/client-island.d.ts +4 -0
  61. package/vite/client-island.js +14 -0
  62. package/vite/code-splitting.d.ts +4 -0
  63. package/vite/code-splitting.js +14 -0
  64. package/vite/index.d.ts +3 -0
  65. package/vite/index.js +3 -0
  66. package/vite/router.d.ts +2 -0
  67. package/vite/router.js +29 -0
  68. package/bin/cli/config.d.ts +0 -10
  69. package/bin/cli/config.js +0 -4
  70. package/bin/cli/index.js +0 -66
  71. package/bin/client/entry.d.ts +0 -1
  72. package/bin/client/index.d.ts +0 -7
  73. package/bin/client/watch.d.ts +0 -1
  74. package/bin/client/watch.js +0 -11
  75. package/bin/index.d.ts +0 -11
  76. package/bin/index.js +0 -18
  77. package/bin/request/index.js +0 -6
  78. package/bin/response.d.ts +0 -9
  79. package/bin/response.js +0 -46
  80. package/bin/types.d.ts +0 -10
  81. package/bin/types.js +0 -1
  82. package/bin/util/event-source.d.ts +0 -16
  83. package/bin/util/event-source.js +0 -85
  84. package/bin/util/index.d.ts +0 -1
  85. package/bin/util/index.js +0 -7
  86. /package/{bin/util/cookies.d.ts → cookies.d.ts} +0 -0
  87. /package/{bin/util/cookies.js → cookies.js} +0 -0
  88. /package/{bin/util/css.d.ts → css.d.ts} +0 -0
  89. /package/{bin → internal}/cli/index.d.ts +0 -0
  90. /package/{bin/util → internal}/hash.d.ts +0 -0
  91. /package/{bin/util → internal}/hash.js +0 -0
  92. /package/{bin/client → internal}/mount.d.ts +0 -0
  93. /package/{bin/util/shell.d.ts → shell.d.ts} +0 -0
  94. /package/{bin/util → util}/path-builder.d.ts +0 -0
package/readme.md CHANGED
@@ -1,217 +1,5 @@
1
1
  # htmx Router
2
2
 
3
- A simple file based router with support for dynamic + client islands, route-less endpoints, and built in CSS sheet generation.
3
+ A simple file based router with support for: dynamic + client islands; route-less endpoints; client/server bundle spitting, and built in CSS sheet generation.
4
4
 
5
- - [htmx Router](#htmx-router)
6
- - [Setup](#setup)
7
- - [Routing](#routing)
8
- - [Route Module](#route-module)
9
- - [Nested Route Rendering](#nested-route-rendering)
10
- - [JSX Rendering](#jsx-rendering)
11
- - [Route Contexts](#route-contexts)
12
- - [Params](#params)
13
- - [Cookies](#cookies)
14
- - [Headers](#headers)
15
- - [Request](#request)
16
- - [URL](#url)
17
- - [Style Sheets](#style-sheets)
18
- - [Route-less Endpoint](#route-less-endpoint)
19
- - [Islands](#islands)
20
- - [Dynamic](#dynamic)
21
- - [Client](#client)
22
-
23
-
24
- ## Setup
25
-
26
- Create a `htmx-router.json` in the root of your project defining where you want your router file to be placed, and where your routes are.
27
- You can also define where you want to create your client component bindings, and for what target framework.
28
- ```json
29
- {
30
- "router": {
31
- "folder": "./source/routes",
32
- "output": "./source/router.tsx"
33
- },
34
- "client": {
35
- "adapter": "react",
36
- "source": "./source/client.tsx"
37
- }
38
- }
39
- ```
40
-
41
- Once you have done this, run `npx htmx-router` to generate the artifacts to start development.
42
- We recommand you copy the setup from `examples/react` for your `server.js`, `entry.server.ts`, and `entry.client.ts`.
43
-
44
- Don't forget that in all rendered routes you must include the `<RouterHeader/>` component in your head for hydration and `StyleClass`s to apply affectively.
45
-
46
-
47
- ## Routing
48
-
49
- Routing applies in a depth first order, where it will match in order:
50
- 1. The `_index`
51
- 2. Static sub-routes
52
- 3. Url path-param wildcards
53
- 4. Catchall slug
54
-
55
- This allows for easy overriding and fallback behaviour. For instance with the routes.
56
- ```
57
- /user/$id.tsx
58
- /user/me.tsx
59
- /user/$.tsx
60
- ```
61
-
62
- It will match on the `/user/me` route if applicable, and otherwise will fallback to attempt to match on `/user/$id`, and if the wildcard route fails, it will try the generic slug route `/user/$.tsx`.
63
-
64
- If a route returns `null` the router will continue the depth first search, allowing for dynamic flow through of the routes.
65
-
66
- ### Route Module
67
-
68
- A route module must define a `parameters` export, which defines how the url path params should be parsed when attempting to match the route.
69
- You can use any function which takes a string, and returns something as the parser. You can also simply use JS-Builtin functions for this, and there is a special case with the `Number` function so it will reject on `NaN` values.
70
- ```js
71
- export const parameters = { id: Number };
72
- ```
73
-
74
- A route can additionally define a loader, which is called on `GET` and `HEAD` requests
75
- ```ts
76
- export async function loader({}: RouteContext<typeof parameters>);
77
- ```
78
-
79
- With the `action` function being called for all other methods
80
- ```ts
81
- export async function action({}: RouteContext<typeof parameters>);
82
- ```
83
-
84
- If any value is thrown by the parameter parsing, or the render functions (`loader`/`action`) it will boil up, attempting first to call an error function is supplied in the route, and otherwise boiling up to the nearest slug route's `error` function.
85
- ```ts
86
- export async function error(ctx: GenericContext, error: unknown);
87
- ```
88
-
89
- ### Nested Route Rendering
90
-
91
- The router will not do nested layouts, if that behaviour is required we recommend using the slug-shell pattern.
92
- Where you define a slug route, and export a `shell` function which takes the `JSX` rendered result from the sub route, and renders the upper route around it.
93
-
94
- This allows flexibility at runtime on how nested route rendering behaves, and can also allow you to ensure you are not reloading data from the db which is already loaded by a sub-route based on how you parse up data through your slug shells.
95
-
96
- We recommend you look at [Predictable Bot](https://github.com/AjaniBilby/predictable) as an example of this pattern performed simply.
97
-
98
-
99
- ### JSX Rendering
100
-
101
- htmx-router is jsx templating agnostic for SSR, instead only requiring a definition provided when creating your request handler, allowing you to BYO JSX templating.
102
- ```js
103
- // @kitajs/html
104
- app.use('*', createRequestHandler.http({
105
- build,
106
- viteDevServer,
107
- render: (res) => {
108
- const headers = new Headers();
109
- headers.set("Content-Type", "text/html; charset=UTF-8");
110
- return new Response(String(res), { headers });
111
- }
112
- }));
113
-
114
- // React
115
- app.use('*', createRequestHandler.http({
116
- build,
117
- viteDevServer,
118
- render: (res) => {
119
- const headers = new Headers();
120
- headers.set("Content-Type", "text/html; charset=UTF-8");
121
- return new Response(renderToString(res), { headers });
122
- }
123
- }));
124
- ```
125
-
126
- ## Route Contexts
127
-
128
- There are two kinds of route context, the `RouteContext<T>` which is the resolved route with parameterization, and the `GenericContext` which is used by error functions, and dynamic loads.
129
-
130
- ### Params
131
-
132
- In the `GenericContext` this will simply be an object with string key value pairs for the parameters, and only the `RouteContext<T>` for `loader` and `action` will have the parameters pre-parsed by your `parameters` definition.
133
-
134
- ### Cookies
135
-
136
- The `RouteContext` and `GenericContext`s both provide a `cookie` object, with the cookie's pre-parsed from the request headers.
137
- It also has a built in `set(name, value, options)` function which will add the appropriate headers to the response for the cookie changes.
138
-
139
- ### Headers
140
-
141
- This is a header object useful for adding response headers when you haven't fully finished generating your response yet.
142
- These headers will merge with the response object created by the provided `render` function, with response headers overriding any conflicting `ctx.headers` values.
143
-
144
- ### Request
145
-
146
- This is the original request object, including request headers.
147
-
148
- ### URL
149
-
150
- The parsed `URL` object of the incoming request.
151
-
152
- ## Style Sheets
153
-
154
- htmx-router includes a `StyleClass` object, which can be used to define CSS classes without needing a unique name.
155
- StyleClasses should only be defined at the top level of a file, and not created within a function, or dynamically during runtime.
156
-
157
- ```ts
158
- const myClass = new StyleClass(`myClass`, `
159
- .this:hover {
160
- background-color: red;
161
- }
162
- `).name;
163
- ```
164
-
165
- ## Route-less Endpoint
166
-
167
- This should be defined at the top level of your file, these endpoints can optionally be given an name which will help for debugging network requests, but they do not need to be unique.
168
- ```ts
169
- const endpoint_url = new Endpoint((ctx: GenericContext) => {
170
- return new Response("Hello World");
171
- }, "hello-world").url;
172
- ```
173
-
174
- ## Islands
175
-
176
- > Tip: Don't forget to wrap your islands in a hx-preserve to prevent losing state. And use `display: contents;` to make that wrapping div transparent for grid and other layout features.
177
-
178
- ### Dynamic
179
-
180
- A dynamic component takes params which will be converted into the props of the loader function, these props may only be string key string value pairs as they are encoded the the query string to allow for browser side caching.
181
-
182
- The body of a dynamic component is the pre-rendered infill that will display while the client is loading the dynamic content.
183
-
184
- ```tsx
185
- async function MyProfile(params: {}, ctx: GenericContext): Promise<JSX.Element> {
186
- ctx.headers.set('Cache-Control', "private, max-age=120");
187
- const userID = ctx.cookie.get('userID');
188
- if (!userID) return <></>;
189
-
190
- const user = await GetUser(userID);
191
- if (!user) return <></>;
192
-
193
- return <a href={`/user/${userID}`}>
194
- <div safe>{user.name}</div>
195
- </a>
196
- }
197
-
198
- export async function loader({ params }: RouteContext<typeof parameters>) {
199
- return <Dynamic params={{}} loader={MyProfile}>
200
- put your ssr pre-rendered skeleton here
201
- </Dynamic>
202
- }
203
- ```
204
-
205
- ### Client
206
-
207
- Import all of the components you want to be able to use on the client side into your `client.tsx`, if you are running a dev server this file will automatically generate the clientized version, otherwise use the `npx htmx-router` command to regenerate these artifacts.
208
-
209
- Once a component has been clientized you can import it as use it like normal, however the body is now overwritten to it will render immediately on the server, and then all props will parsed to the client for it to be rendered properly in the browser.
210
-
211
- ```tsx
212
- <Client.Counter>
213
- <button>No yet hydrated...</button> {/* this will be overwritten in the browser once hydrated */}
214
- </Client.Counter>
215
- ```
216
-
217
- It is very important that you ensure your `Client` component has a single child element, if there are multiple child components the browser will only mount to the last child causing artifacting.
5
+ > Apologies the docs have not yet been updated for this version, please wait for the final release for actual docs.
@@ -1,6 +1,6 @@
1
1
  import type { IncomingMessage, ServerResponse } from "http";
2
2
  import type { ViteDevServer } from "vite";
3
- import { GenericContext } from "../router.js";
3
+ import type { GenericContext } from "~/router.js";
4
4
  type Config = {
5
5
  build: Promise<any> | (() => Promise<Record<string, any>>);
6
6
  viteDevServer: ViteDevServer | null;
@@ -1,4 +1,4 @@
1
- import { Resolve } from "../request/native.js";
1
+ import { Resolve } from "~/request/native.js";
2
2
  export function createRequestHandler(config) {
3
3
  return async (req, res) => {
4
4
  try {
@@ -6,8 +6,20 @@ export function createRequestHandler(config) {
6
6
  const request = NativeRequest(req);
7
7
  let { response, headers } = await Resolve(request, mod.tree, config);
8
8
  res.writeHead(response.status, headers);
9
- let rendered = await response.text();
10
- res.end(rendered);
9
+ if (response.body instanceof ReadableStream) {
10
+ const reader = response.body.getReader();
11
+ while (true) {
12
+ const { done, value } = await reader.read();
13
+ if (done)
14
+ break;
15
+ res.write(value); // `value` is a Uint8Array.
16
+ }
17
+ res.end();
18
+ }
19
+ else {
20
+ const rendered = await response.text();
21
+ res.end(rendered);
22
+ }
11
23
  }
12
24
  catch (e) {
13
25
  res.statusCode = 500;
@@ -29,7 +41,7 @@ function NativeRequest(req) {
29
41
  const url = new URL(`http://${headers.get('host')}${req.originalUrl || req.url}`);
30
42
  req.once('aborted', () => ctrl.abort());
31
43
  const bodied = req.method !== "GET" && req.method !== "HEAD";
32
- return new Request(url, {
44
+ const request = new Request(url, {
33
45
  headers,
34
46
  method: req.method,
35
47
  body: bodied ? req : undefined,
@@ -38,4 +50,10 @@ function NativeRequest(req) {
38
50
  // @ts-ignore
39
51
  duplex: bodied ? 'half' : undefined
40
52
  });
53
+ if (!request.headers.has("X-Real-IP")) {
54
+ const info = req.socket.address();
55
+ if ("address" in info)
56
+ request.headers.set("X-Real-IP", info.address);
57
+ }
58
+ return request;
41
59
  }
@@ -0,0 +1,13 @@
1
+ import type { ViteDevServer } from "vite";
2
+ import type { GenericContext, RouteTree } from '~/router.js';
3
+ import * as native from "~/request/native.js";
4
+ import * as http from "~/request/http.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 { http, native };
@@ -0,0 +1,3 @@
1
+ import * as native from "~/request/native.js";
2
+ import * as http from "~/request/http.js";
3
+ export { http, native };
@@ -0,0 +1,9 @@
1
+ import { type RouteTree } from '~/router.js';
2
+ import type { 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
+ }>;
@@ -1,4 +1,4 @@
1
- import { GenericContext } from '../router.js';
1
+ import { GenericContext } from '~/router.js';
2
2
  export function createRequestHandler(config) {
3
3
  return async (req) => {
4
4
  try {
@@ -30,7 +30,7 @@ export async function Resolve(request, tree, config) {
30
30
  // Override with context headers
31
31
  if (response.headers !== ctx.headers) {
32
32
  for (const [key, value] of ctx.headers) {
33
- if (ctx.headers.has(key))
33
+ if (response.headers.has(key))
34
34
  continue;
35
35
  response.headers.set(key, value);
36
36
  }
@@ -1,7 +1,9 @@
1
1
  export declare function text(text: string, init?: ResponseInit): Response;
2
- export declare function json<T>(data: T, init?: ResponseInit): Omit<Response, "json"> & {
2
+ export type TypedResponse<T> = Omit<Response, "json"> & {
3
3
  json(): Promise<T>;
4
4
  };
5
+ export type TypedJson<U extends TypedResponse<any>> = U extends TypedResponse<infer T> ? T : never;
6
+ export declare function json<T>(data: T, init?: ResponseInit): TypedResponse<T>;
5
7
  export declare function redirect(url: string, init?: ResponseInit & {
6
8
  clientOnly?: boolean;
7
9
  }): Response;
@@ -1,7 +1,7 @@
1
1
  export function text(text, init) {
2
2
  init ||= {};
3
3
  init.statusText ||= "ok";
4
- init.status = 200;
4
+ init.status ||= 200;
5
5
  const res = new Response(text, init);
6
6
  res.headers.set("Content-Type", "text/plain");
7
7
  res.headers.set("X-Caught", "true");
@@ -10,7 +10,7 @@ export function text(text, init) {
10
10
  export function json(data, init) {
11
11
  init ||= {};
12
12
  init.statusText ||= "ok";
13
- init.status = 200;
13
+ init.status ||= 200;
14
14
  const res = new Response(JSON.stringify(data), init);
15
15
  res.headers.set("Content-Type", "application/json");
16
16
  res.headers.set("X-Caught", "true");
@@ -19,7 +19,7 @@ export function json(data, init) {
19
19
  export function redirect(url, init) {
20
20
  init ||= {};
21
21
  init.statusText ||= "Temporary Redirect";
22
- init.status = 307;
22
+ init.status ||= 307;
23
23
  const res = new Response("", init);
24
24
  if (!init?.clientOnly)
25
25
  res.headers.set("Location", url);
@@ -29,7 +29,7 @@ export function redirect(url, init) {
29
29
  export function revalidate(init) {
30
30
  init ||= {};
31
31
  init.statusText ||= "ok";
32
- init.status = 200;
32
+ init.status ||= 200;
33
33
  const res = new Response("", init);
34
34
  res.headers.set("HX-Location", "");
35
35
  return res;
@@ -37,7 +37,7 @@ export function revalidate(init) {
37
37
  export function refresh(init) {
38
38
  init ||= {};
39
39
  init.statusText ||= "ok";
40
- init.status = 200;
40
+ init.status ||= 200;
41
41
  const res = new Response("", init);
42
42
  if (!init?.clientOnly)
43
43
  res.headers.set("Refresh", "0"); // fallback
@@ -1,6 +1,7 @@
1
1
  import { Parameterized, ParameterShaper } from './util/parameters.js';
2
- import { RouteModule } from "./types.js";
3
- import { Cookies } from './util/cookies.js';
2
+ import { RouteModule } from "./index.js";
3
+ import { Cookies } from './cookies.js';
4
+ export declare function GenerateRouteTree(modules: Record<string, unknown>): RouteTree;
4
5
  export declare class GenericContext {
5
6
  request: Request;
6
7
  headers: Headers;
@@ -13,7 +14,7 @@ export declare class GenericContext {
13
14
  constructor(request: GenericContext["request"], url: GenericContext["url"], renderer: GenericContext["render"]);
14
15
  shape<T extends ParameterShaper>(shape: T): RouteContext<T>;
15
16
  }
16
- export declare class RouteContext<T extends ParameterShaper> {
17
+ export declare class RouteContext<T extends ParameterShaper = {}> {
17
18
  request: Request;
18
19
  headers: Headers;
19
20
  cookie: Cookies;
@@ -22,13 +23,6 @@ export declare class RouteContext<T extends ParameterShaper> {
22
23
  render: (res: JSX.Element) => Response;
23
24
  constructor(base: GenericContext, shape: T);
24
25
  }
25
- export declare class RouteLeaf {
26
- module: RouteModule<any>;
27
- constructor(module: RouteModule<any>);
28
- resolve(ctx: GenericContext): Promise<Response | null>;
29
- error(ctx: GenericContext, e: unknown): Promise<Response>;
30
- private renderWrapper;
31
- }
32
26
  export declare class RouteTree {
33
27
  root: boolean;
34
28
  nested: Map<string, RouteTree>;
@@ -47,3 +41,11 @@ export declare class RouteTree {
47
41
  private resolveNative;
48
42
  private unwrap;
49
43
  }
44
+ declare class RouteLeaf {
45
+ module: RouteModule<any>;
46
+ constructor(module: RouteModule<any>);
47
+ resolve(ctx: GenericContext): Promise<Response | null>;
48
+ error(ctx: GenericContext, e: unknown): Promise<Response>;
49
+ private renderWrapper;
50
+ }
51
+ export {};
@@ -1,9 +1,23 @@
1
- import * as endpoint from './util/endpoint.js';
2
- import * as dynamic from './util/dynamic.js';
3
- import * as mount from './client/mount.js';
4
- import * as css from './util/css.js';
1
+ import { ServerOnlyWarning } from "./internal/util.js";
2
+ ServerOnlyWarning("router");
3
+ import * as endpoint from './endpoint.js';
4
+ import * as dynamic from './dynamic.js';
5
+ import * as mount from './internal/mount.js';
6
+ import * as css from './css.js';
5
7
  import { Parameterize } from './util/parameters.js';
6
- import { Cookies } from './util/cookies.js';
8
+ import { Cookies } from './cookies.js';
9
+ export function GenerateRouteTree(modules) {
10
+ const tree = new RouteTree();
11
+ for (const path in modules) {
12
+ const mod = modules[path];
13
+ const tail = path.lastIndexOf(".");
14
+ const url = path.slice(9, tail);
15
+ tree.ingest(url, mod);
16
+ if (mod.route)
17
+ mod.route(url);
18
+ }
19
+ return tree;
20
+ }
7
21
  export class GenericContext {
8
22
  request;
9
23
  headers; // response headers
@@ -40,48 +54,6 @@ export class RouteContext {
40
54
  this.url = base.url;
41
55
  }
42
56
  }
43
- export class RouteLeaf {
44
- module;
45
- constructor(module) {
46
- this.module = module;
47
- }
48
- async resolve(ctx) {
49
- const res = await this.renderWrapper(ctx);
50
- if (res === null)
51
- return null;
52
- if (res instanceof Response)
53
- return res;
54
- return ctx.render(res);
55
- }
56
- async error(ctx, e) {
57
- if (!this.module.error)
58
- throw e;
59
- const res = await this.module.error(ctx, e);
60
- if (res instanceof Response)
61
- return res;
62
- return ctx.render(res);
63
- }
64
- async renderWrapper(ctx) {
65
- try {
66
- if (!this.module.loader && !this.module.action)
67
- return null;
68
- const context = ctx.shape(this.module.parameters || {});
69
- if (ctx.request.method === "HEAD" || ctx.request.method === "GET") {
70
- if (this.module.loader)
71
- return await this.module.loader(context);
72
- else
73
- return null;
74
- }
75
- if (this.module.action)
76
- return await this.module.action(context);
77
- throw new Response("Method not Allowed", { status: 405, statusText: "Method not Allowed", headers: ctx.headers });
78
- }
79
- catch (e) {
80
- return await this.error(ctx, e);
81
- }
82
- return null;
83
- }
84
- }
85
57
  export class RouteTree {
86
58
  root;
87
59
  nested;
@@ -206,6 +178,48 @@ export class RouteTree {
206
178
  return caught;
207
179
  }
208
180
  }
181
+ class RouteLeaf {
182
+ module;
183
+ constructor(module) {
184
+ this.module = module;
185
+ }
186
+ async resolve(ctx) {
187
+ const res = await this.renderWrapper(ctx);
188
+ if (res === null)
189
+ return null;
190
+ if (res instanceof Response)
191
+ return res;
192
+ return ctx.render(res);
193
+ }
194
+ async error(ctx, e) {
195
+ if (!this.module.error)
196
+ throw e;
197
+ const res = await this.module.error(ctx, e);
198
+ if (res instanceof Response)
199
+ return res;
200
+ return ctx.render(res);
201
+ }
202
+ async renderWrapper(ctx) {
203
+ try {
204
+ if (!this.module.loader && !this.module.action)
205
+ return null;
206
+ const context = ctx.shape(this.module.parameters || {});
207
+ if (ctx.request.method === "HEAD" || ctx.request.method === "GET") {
208
+ if (this.module.loader)
209
+ return await this.module.loader(context);
210
+ else
211
+ return null;
212
+ }
213
+ if (this.module.action)
214
+ return await this.module.action(context);
215
+ throw new Response("Method not Allowed", { status: 405, statusText: "Method not Allowed", headers: ctx.headers });
216
+ }
217
+ catch (e) {
218
+ return await this.error(ctx, e);
219
+ }
220
+ return null;
221
+ }
222
+ }
209
223
  async function ResolveNatively(fragments, ctx) {
210
224
  switch (fragments[1]) {
211
225
  case "dynamic": return dynamic._resolve(fragments, ctx);
@@ -1,3 +1,5 @@
1
+ import { ServerOnlyWarning } from "./internal/util.js";
2
+ ServerOnlyWarning("shell");
1
3
  export function ApplyMetaDescriptorDefaults(options, defaults) {
2
4
  if (defaults.title && !options.title)
3
5
  options.title = defaults.title;
@@ -22,7 +24,7 @@ export function RenderMetaDescriptor(options) {
22
24
  }
23
25
  if (options.jsonLD)
24
26
  for (const json of options.jsonLD) {
25
- out += `<script>${EscapeHTML(JSON.stringify(json))}</script>\n`;
27
+ out += `<script type="application/ld+json">${JSON.stringify(json)}</script>\n`;
26
28
  }
27
29
  // Auto apply og:title + og:description if not present
28
30
  if (options.title && !options.og?.title)
@@ -1,6 +1,3 @@
1
- /**
2
- * This is used by GenericContext to convert itself to a RouteContext<T>
3
- */
4
1
  export type Parameterized<T extends ParameterShaper> = {
5
2
  [K in keyof T]: ReturnType<T[K]>;
6
3
  };
@@ -1,6 +1,3 @@
1
- /**
2
- * This is used by GenericContext to convert itself to a RouteContext<T>
3
- */
4
1
  export function Parameterize(params, shape) {
5
2
  const out = {};
6
3
  for (const key in shape) {
@@ -1,3 +1,5 @@
1
+ import { ServerOnlyWarning } from "../internal/util.js";
2
+ ServerOnlyWarning("path-builder");
1
3
  import { relative } from "path";
2
4
  /*
3
5
  // This feature is disabled because vite doesn't compile import.meta.url to the original url when making the SSR build
@@ -0,0 +1,2 @@
1
+ import { ParameterShaper } from "./parameters.js";
2
+ export declare function RoutePath<T extends ParameterShaper>(): (params: { [K in keyof T]: string; }) => string;
package/util/route.js ADDED
@@ -0,0 +1,58 @@
1
+ import { ServerOnlyWarning } from "../internal/util.js";
2
+ ServerOnlyWarning("route-path");
3
+ export function RoutePath() {
4
+ const frags = new Array();
5
+ return (params) => {
6
+ const t = params;
7
+ if (typeof t === "string") {
8
+ Compile("/" + t, frags);
9
+ return "";
10
+ }
11
+ return Parse(frags, params);
12
+ };
13
+ }
14
+ const indexRoute = "/_index";
15
+ function Compile(url, into) {
16
+ let cursor = 0;
17
+ let i = 1;
18
+ for (; i < url.length; i++) {
19
+ if (url[i] !== "$")
20
+ continue;
21
+ if (url[i - 1] !== "/")
22
+ continue;
23
+ let e = url.indexOf("/", i + 1);
24
+ if (e === -1)
25
+ e = url.length;
26
+ into.push(url.slice(cursor, i));
27
+ into.push(url.slice(i, e));
28
+ cursor = e;
29
+ i = e - 1;
30
+ }
31
+ // remainder
32
+ if (cursor != i)
33
+ into.push(url.slice(cursor));
34
+ // remove _index from end if present
35
+ const lastI = into.length - 1;
36
+ const last = into[lastI];
37
+ if (last && last.endsWith(indexRoute)) {
38
+ if (last.length === indexRoute.length)
39
+ into.length--;
40
+ else
41
+ into[lastI] = last.slice(0, -indexRoute.length);
42
+ }
43
+ }
44
+ function Parse(fragments, params) {
45
+ let out = "";
46
+ for (const frag of fragments) {
47
+ if (!frag.startsWith("$")) {
48
+ out += frag;
49
+ continue;
50
+ }
51
+ const key = frag.slice(1);
52
+ const param = params[key];
53
+ if (!param)
54
+ throw new Error(`Missing ${key} parameter required for route`);
55
+ out += param;
56
+ }
57
+ return out;
58
+ }
@@ -0,0 +1,4 @@
1
+ import type { UserConfig } from "vite";
2
+ type Plugin = NonNullable<UserConfig["plugins"]>[number];
3
+ export declare function BundleSplitter(): Plugin;
4
+ export {};