@torpor/build 0.4.14 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/README.md +20 -2
  2. package/dist/Jsonify-Dg6gChd3.d.mts +10 -0
  3. package/dist/Jsonify-Dg6gChd3.d.mts.map +1 -0
  4. package/dist/ParseRouteParams-DBmqlmLh.d.mts +42 -0
  5. package/dist/ParseRouteParams-DBmqlmLh.d.mts.map +1 -0
  6. package/dist/{RouteType-B6BMOyYw.mjs → Router-C4ADUfhv.mjs} +30 -42
  7. package/dist/Router-C4ADUfhv.mjs.map +1 -0
  8. package/dist/{Server-CF5wDJp6.d.mts → Server-C_IKav_e.d.mts} +44 -16
  9. package/dist/Server-C_IKav_e.d.mts.map +1 -0
  10. package/dist/{ServerEvent-CxPwA4lH.mjs → ServerEvent-Dljn5i7A.mjs} +37 -11
  11. package/dist/ServerEvent-Dljn5i7A.mjs.map +1 -0
  12. package/dist/Site-CMa153FA.d.mts +565 -0
  13. package/dist/Site-CMa153FA.d.mts.map +1 -0
  14. package/dist/StandardSchema-D28L8_zZ.d.mts +100 -0
  15. package/dist/StandardSchema-D28L8_zZ.d.mts.map +1 -0
  16. package/dist/TypedResponse-6lznqN4U.d.mts +22 -0
  17. package/dist/TypedResponse-6lznqN4U.d.mts.map +1 -0
  18. package/dist/{_page-BQs4WvCO.mjs → _page-4cQKs_Dn.mjs} +3 -5
  19. package/dist/_page-4cQKs_Dn.mjs.map +1 -0
  20. package/dist/bin/index.d.mts +1 -1
  21. package/dist/bin/index.js +15 -11
  22. package/dist/bin/index.js.map +1 -1
  23. package/dist/flattenHeaders-C_YYLdOq.mjs +114 -0
  24. package/dist/flattenHeaders-C_YYLdOq.mjs.map +1 -0
  25. package/dist/form.d.mts +35 -0
  26. package/dist/form.d.mts.map +1 -0
  27. package/dist/form.mjs +48 -0
  28. package/dist/form.mjs.map +1 -0
  29. package/dist/index.d.mts +72 -156
  30. package/dist/index.d.mts.map +1 -1
  31. package/dist/index.mjs +158 -26
  32. package/dist/index.mjs.map +1 -1
  33. package/dist/nav.d.mts +58 -5
  34. package/dist/nav.d.mts.map +1 -1
  35. package/dist/nav.mjs +116 -38
  36. package/dist/nav.mjs.map +1 -1
  37. package/dist/openapi.d.mts +144 -0
  38. package/dist/openapi.d.mts.map +1 -0
  39. package/dist/openapi.mjs +2 -0
  40. package/dist/pathTrie-D4Ax2Mu9.mjs +160 -0
  41. package/dist/pathTrie-D4Ax2Mu9.mjs.map +1 -0
  42. package/dist/plugin-D8Www11B.mjs +202 -0
  43. package/dist/plugin-D8Www11B.mjs.map +1 -0
  44. package/dist/{response-C5TtAsh1.mjs → response-Bg4w2S1q.mjs} +16 -39
  45. package/dist/response-Bg4w2S1q.mjs.map +1 -0
  46. package/dist/response.d.mts +309 -289
  47. package/dist/response.d.mts.map +1 -1
  48. package/dist/response.mjs +3 -4
  49. package/dist/run.d.mts +11 -4
  50. package/dist/run.d.mts.map +1 -1
  51. package/dist/run.mjs +2 -8
  52. package/dist/runOpenApi-BUi1RH1l.mjs +1381 -0
  53. package/dist/runOpenApi-BUi1RH1l.mjs.map +1 -0
  54. package/dist/schema.d.mts +32 -0
  55. package/dist/schema.d.mts.map +1 -0
  56. package/dist/schema.mjs +2 -0
  57. package/dist/server.d.mts +10 -8
  58. package/dist/server.d.mts.map +1 -1
  59. package/dist/server.mjs +132 -4
  60. package/dist/server.mjs.map +1 -0
  61. package/dist/state.d.mts.map +1 -1
  62. package/dist/state.mjs +2 -3
  63. package/dist/test.d.mts +8 -10
  64. package/dist/test.d.mts.map +1 -1
  65. package/dist/test.mjs +283 -79
  66. package/dist/test.mjs.map +1 -1
  67. package/dist/{seeOther-B4Yhu9iq.mjs → unprocessable-DAZbrDeR.mjs} +34 -13
  68. package/dist/unprocessable-DAZbrDeR.mjs.map +1 -0
  69. package/dist/validate-Ok4krlkX.mjs +40 -0
  70. package/dist/validate-Ok4krlkX.mjs.map +1 -0
  71. package/package.json +28 -17
  72. package/src/bin/index.ts +13 -2
  73. package/src/dev.ts +10 -0
  74. package/src/form/formDataToRecord.ts +23 -0
  75. package/src/form/readForm.ts +71 -0
  76. package/src/form.ts +4 -0
  77. package/src/index.ts +53 -1
  78. package/src/nav/api.test-d.ts +79 -0
  79. package/src/nav/api.ts +104 -0
  80. package/src/nav/formSubmit.ts +18 -4
  81. package/src/nav/navigate.ts +40 -15
  82. package/src/nav/route.ts +38 -0
  83. package/src/nav.ts +3 -1
  84. package/src/openapi/docsHtml.ts +25 -0
  85. package/src/openapi/document.ts +154 -0
  86. package/src/openapi/plugin.ts +78 -0
  87. package/src/openapi/types.ts +82 -0
  88. package/src/openapi.ts +14 -0
  89. package/src/response/TypedResponse.ts +17 -0
  90. package/src/response/badRequest.ts +14 -2
  91. package/src/response/created.ts +13 -2
  92. package/src/response/found.ts +2 -2
  93. package/src/response/movedPermanently.ts +2 -2
  94. package/src/response/notModified.ts +2 -2
  95. package/src/response/ok.ts +13 -4
  96. package/src/response/response.ts +4 -4
  97. package/src/response/unprocessable.ts +14 -2
  98. package/src/run/depCache.ts +175 -0
  99. package/src/run/devPlugin.ts +128 -0
  100. package/src/run/prepareTemplate.ts +6 -3
  101. package/src/run/run.ts +63 -39
  102. package/src/run/runBuild.ts +63 -12
  103. package/src/run/runDev.ts +146 -58
  104. package/src/run/runOpenApi.ts +52 -0
  105. package/src/run/runPreview.ts +17 -26
  106. package/src/run/staleTorpCopies.ts +100 -0
  107. package/src/run.ts +2 -1
  108. package/src/schema.ts +7 -0
  109. package/src/server/CookieHelper.ts +17 -7
  110. package/src/server/Server.ts +54 -30
  111. package/src/server/ServerEvent.ts +24 -1
  112. package/src/server/connect/connectMiddleware.ts +44 -40
  113. package/src/server/connect/flattenHeaders.ts +1 -23
  114. package/src/server/connect/requestToNodeMessage.ts +6 -3
  115. package/src/server/types/MiddlewareFunction.ts +18 -4
  116. package/src/site/Router.ts +52 -41
  117. package/src/site/Site.ts +184 -10
  118. package/src/site/checkLayoutSlots.ts +114 -0
  119. package/src/site/checkRoutes.ts +435 -0
  120. package/src/site/clientEntry.ts +22 -9
  121. package/src/site/layoutSlots.ts +53 -0
  122. package/src/site/manifest.ts +125 -6
  123. package/src/site/serverEntry.ts +341 -132
  124. package/src/state/$page.ts +2 -1
  125. package/src/state/$serverPage.ts +22 -0
  126. package/src/test/runTest.ts +306 -118
  127. package/src/types/Adapter.ts +12 -0
  128. package/src/types/Jsonify.ts +20 -0
  129. package/src/types/PageData.test-d.ts +136 -0
  130. package/src/types/PageData.ts +35 -0
  131. package/src/types/PageEndPoint.ts +25 -9
  132. package/src/types/PageForm.test-d.ts +78 -0
  133. package/src/types/PageForm.ts +29 -0
  134. package/src/types/PageLoadEvent.ts +16 -4
  135. package/src/types/PageLoadReturn.ts +13 -0
  136. package/src/types/PageProps.ts +14 -0
  137. package/src/types/PageServerAction.ts +8 -2
  138. package/src/types/PageServerEndPoint.test-d.ts +105 -0
  139. package/src/types/PageServerEndPoint.ts +99 -5
  140. package/src/types/PageServerLoad.ts +11 -3
  141. package/src/types/ParseRouteParams.test-d.ts +84 -0
  142. package/src/types/ParseRouteParams.ts +64 -0
  143. package/src/types/Route.ts +20 -1
  144. package/src/types/RouteHandler.ts +8 -1
  145. package/src/types/ServerEndPoint.test-d.ts +77 -0
  146. package/src/types/ServerEndPoint.ts +148 -16
  147. package/src/types/ServerHook.ts +10 -3
  148. package/src/types/ServerLoadEvent.ts +48 -3
  149. package/src/types/ServerRequest.ts +10 -2
  150. package/src/types/SitePlugin.ts +24 -0
  151. package/src/types/StandardSchema.ts +109 -0
  152. package/src/utils/pathToRegex.ts +4 -2
  153. package/src/utils/pathTrie.ts +182 -0
  154. package/src/utils/searchParamsToRecord.ts +18 -0
  155. package/src/utils/torporPackages.ts +150 -0
  156. package/src/utils/tsconfigAliases.ts +90 -0
  157. package/src/validation/ValidationError.ts +18 -0
  158. package/src/validation/endpoint.ts +83 -0
  159. package/src/validation/validate.ts +26 -0
  160. package/dist/RouteType-B6BMOyYw.mjs.map +0 -1
  161. package/dist/Server-CF5wDJp6.d.mts.map +0 -1
  162. package/dist/ServerEvent-CxPwA4lH.mjs.map +0 -1
  163. package/dist/Site-DgC6WWn1.d.mts +0 -78
  164. package/dist/Site-DgC6WWn1.d.mts.map +0 -1
  165. package/dist/_page-BQs4WvCO.mjs.map +0 -1
  166. package/dist/connectMiddleware-D7nehjj3.mjs +0 -252
  167. package/dist/connectMiddleware-D7nehjj3.mjs.map +0 -1
  168. package/dist/pathToRegex-TUAMc3zS.mjs +0 -11
  169. package/dist/pathToRegex-TUAMc3zS.mjs.map +0 -1
  170. package/dist/response-C5TtAsh1.mjs.map +0 -1
  171. package/dist/run-BPFtSYA3.mjs +0 -332
  172. package/dist/run-BPFtSYA3.mjs.map +0 -1
  173. package/dist/seeOther-B4Yhu9iq.mjs.map +0 -1
  174. package/src/server/Routerx.ts +0 -72
  175. package/src/server/connect/bufferToArrayBuffer.ts +0 -8
  176. package/src/server/connect/readableToBuffer.ts +0 -16
@@ -1,16 +1,19 @@
1
1
  import { type ServerComponent, type ServerSlotRender } from "@torpor/view/ssr";
2
2
  import path from "node:path";
3
+ import formDataToRecord from "../form/formDataToRecord.ts";
3
4
  import notFound from "../response/notFound.ts";
4
5
  import ok from "../response/ok.ts";
5
6
  import seeOther from "../response/seeOther.ts";
6
7
  import ServerEvent from "../server/ServerEvent.ts";
7
8
  import Router from "../site/Router.ts";
8
9
  import Site from "../site/Site";
9
- import $page from "../state/$page.ts";
10
+ import $page from "../state/$serverPage.ts";
10
11
  import type PageEndPoint from "../types/PageEndPoint.ts";
11
12
  import type PageServerEndPoint from "../types/PageServerEndPoint.ts";
12
13
  import type RouteHandler from "../types/RouteHandler.ts";
14
+ import type ServerRequest from "../types/ServerRequest.ts";
13
15
  import {
16
+ ERROR_ROUTE,
14
17
  HOOK_ROUTE,
15
18
  HOOK_SERVER_ROUTE,
16
19
  LAYOUT_ROUTE,
@@ -22,6 +25,15 @@ import {
22
25
  import type ServerEndPoint from "../types/ServerEndPoint.ts";
23
26
  import type ServerHook from "../types/ServerHook.ts";
24
27
  import type ServerLoadEvent from "../types/ServerLoadEvent.ts";
28
+ import searchParamsToRecord from "../utils/searchParamsToRecord.ts";
29
+ import validate from "../validation/validate.ts";
30
+ import ValidationError from "../validation/ValidationError.ts";
31
+ import {
32
+ endpointSchema,
33
+ validateEndpointParams,
34
+ validateHandlerInput,
35
+ validationErrorResponse,
36
+ } from "../validation/endpoint.ts";
25
37
 
26
38
  /**
27
39
  * Runs a site route in test mode
@@ -47,7 +59,19 @@ export default async function runTest(
47
59
  site.routes.map((r) => ({
48
60
  path: r.path,
49
61
  type: r.type,
50
- endPoint: () => import(/* @vite-ignore */ path.join(site.root, r.file)),
62
+ endPoint: async () => {
63
+ if (r.file) {
64
+ const mod = await import(/* @vite-ignore */ path.join(site.root, r.file));
65
+ // A .torp file's default export is a component, so wrap it as
66
+ // a PageEndPoint ({ component }) for the entries
67
+ if (r.file.endsWith(".torp")) {
68
+ return { default: { component: mod.default } };
69
+ }
70
+ return mod;
71
+ }
72
+ // Inline endpoint (no file) — return directly from memory
73
+ return { default: r.endPoint };
74
+ },
51
75
  subFolder: r.subFolder,
52
76
  })),
53
77
  );
@@ -59,7 +83,7 @@ export default async function runTest(
59
83
  // TODO: Make it shared instead!
60
84
  async function load(router: Router, ev: ServerEvent, template: string): Promise<Response> {
61
85
  //const url = new URL(`http://${process.env.HOST ?? "localhost"}${ev.request.url}`);
62
- const url = new URL(ev.request.url);
86
+ const url = ev.url;
63
87
  const path = url.pathname;
64
88
  const query = url.searchParams;
65
89
 
@@ -73,6 +97,12 @@ async function load(router: Router, ev: ServerEvent, template: string): Promise<
73
97
  // Update $page before building the components
74
98
  // TODO: Find somewhere better to put this
75
99
  $page.url = url;
100
+ if (path.endsWith("/_error")) {
101
+ $page.status = parseInt(query.get("status") ?? "404");
102
+ $page.error = { message: query.get("message") ?? "" };
103
+ } else {
104
+ $page.status = 200;
105
+ }
76
106
 
77
107
  const handler = route.handler;
78
108
  const params = route.params || {};
@@ -87,7 +117,9 @@ async function load(router: Router, ev: ServerEvent, template: string): Promise<
87
117
  if (ev.request.method === "GET") {
88
118
  return await loadView(ev, url, handler, params, template);
89
119
  } else if (ev.request.method === "POST") {
90
- return await runAction(ev, url, handler, params, query);
120
+ const serverEndPoint: PageServerEndPoint | undefined =
121
+ handler.serverEndPoint && (await handler.serverEndPoint()).default;
122
+ return await runAction(ev, url, handler, serverEndPoint, params, query);
91
123
  }
92
124
  break;
93
125
  }
@@ -96,6 +128,10 @@ async function load(router: Router, ev: ServerEvent, template: string): Promise<
96
128
  // It's a /+page.server.ts or /_layout.server.ts endpoint
97
129
  if (ev.request.method === "GET") {
98
130
  return await loadData(ev, url, handler, "load", params);
131
+ } else if (ev.request.method === "POST") {
132
+ const serverEndPoint: PageServerEndPoint | undefined =
133
+ handler.endPoint && (await handler.endPoint()).default;
134
+ return await runAction(ev, url, handler, serverEndPoint, params, query);
99
135
  }
100
136
  break;
101
137
  }
@@ -104,6 +140,9 @@ async function load(router: Router, ev: ServerEvent, template: string): Promise<
104
140
  const functionName = ev.request.method.toLowerCase().replace("delete", "del");
105
141
  return await loadData(ev, url, handler, functionName, params);
106
142
  }
143
+ case ERROR_ROUTE: {
144
+ return await loadView(ev, url, handler, params, template);
145
+ }
107
146
  case HOOK_ROUTE:
108
147
  case HOOK_SERVER_ROUTE: {
109
148
  break;
@@ -118,24 +157,62 @@ async function loadData(
118
157
  url: URL,
119
158
  handler: RouteHandler,
120
159
  functionName: string,
121
- params: Record<string, string>,
160
+ params: Record<string, any>,
122
161
  ) {
123
162
  const serverEndPoint: ServerEndPoint | undefined = (await handler.endPoint()).default;
124
- if (serverEndPoint && serverEndPoint[functionName]) {
125
- const serverParams = buildServerParams(ev, url, params);
126
-
127
- if (handler.serverHook) {
128
- const serverHook: ServerHook | undefined = (await handler.serverHook()).default;
129
- if (serverHook?.handle) {
130
- await serverHook.handle(serverParams);
131
- }
163
+ // `functionName` is computed from the request method, so the handlers are
164
+ // accessed through a loose record
165
+ const handlers = serverEndPoint as unknown as Record<string, ServerRequest | undefined>;
166
+ const handlerFn: ServerRequest | undefined = handlers?.[functionName];
167
+ if (serverEndPoint && handlerFn) {
168
+ // Route params come from the URL, so a failed validation means the
169
+ // resource doesn't exist
170
+ try {
171
+ params = await validateEndpointParams(serverEndPoint, params);
172
+ } catch (error) {
173
+ if (error instanceof ValidationError) return notFound();
174
+ throw error;
132
175
  }
133
176
 
134
- const result = await serverEndPoint[functionName](serverParams);
177
+ try {
178
+ // If the endpoint declares a schema for this handler, validate its
179
+ // input up front and reject with 422 if it fails. get/head schemas
180
+ // validate the query string; other schemas validate the json body
181
+ const schema = endpointSchema(serverEndPoint, functionName);
182
+ const values = await validateHandlerInput(schema, functionName, url, ev);
183
+
184
+ const serverParams = buildServerParams(ev, url, params, values);
185
+
186
+ const serverHooks = await loadServerHooks(handler);
187
+ let entered = 0;
188
+ let enterResponse: Response | undefined = undefined;
189
+ try {
190
+ // Hooks run from the root down; a hook's enter can return a
191
+ // Response to short-circuit the request
192
+ for (; entered < serverHooks.length && !enterResponse; entered++) {
193
+ const hookResult = await serverHooks[entered].enter?.(serverParams);
194
+ if (hookResult) {
195
+ enterResponse = hookResult;
196
+ }
197
+ }
198
+
199
+ const result = enterResponse || (await handlerFn(serverParams));
135
200
 
136
- // If there was no response returned from load (such as errors or a
137
- // redirect), send an ok response
138
- return result || ok();
201
+ // If there was no response returned from load (such as errors or a
202
+ // redirect), send an ok response
203
+ return result || ok();
204
+ } finally {
205
+ // Run exit hooks in reverse order for hooks that were entered
206
+ while (entered > 0) {
207
+ entered--;
208
+ await serverHooks[entered].exit?.(serverParams);
209
+ }
210
+ }
211
+ } catch (error) {
212
+ const response = validationErrorResponse(error);
213
+ if (response) return response;
214
+ throw error;
215
+ }
139
216
  }
140
217
 
141
218
  return notFound();
@@ -145,7 +222,7 @@ async function loadView(
145
222
  ev: ServerEvent,
146
223
  url: URL,
147
224
  handler: RouteHandler,
148
- params: Record<string, string>,
225
+ params: Record<string, any>,
149
226
  template: string,
150
227
  ) {
151
228
  // There must be a client endpoint with a component
@@ -158,129 +235,214 @@ async function loadView(
158
235
  const serverEndPoint: PageServerEndPoint | undefined =
159
236
  handler.serverEndPoint && (await handler.serverEndPoint())?.default;
160
237
 
161
- // Maybe hit the server hook
162
- const serverParams = buildServerParams(ev, url, params);
163
- if (handler.serverHook) {
164
- const serverHook: ServerHook | undefined = (await handler.serverHook()).default;
165
- if (serverHook?.handle) {
166
- await serverHook.handle(serverParams);
238
+ // Validate the route params and the load query against the server
239
+ // endpoint's schemas, if it declares any. Params come from the URL, so a
240
+ // failed validation means the resource doesn't exist; a failed query
241
+ // returns like any other load failure
242
+ let query: unknown;
243
+ if (serverEndPoint) {
244
+ try {
245
+ params = await validateEndpointParams(serverEndPoint, params);
246
+ } catch (error) {
247
+ if (error instanceof ValidationError) return notFound();
248
+ throw error;
249
+ }
250
+ const loadSchema = endpointSchema(serverEndPoint, "load");
251
+ if (loadSchema) {
252
+ try {
253
+ query = await validate(loadSchema, searchParamsToRecord(url.searchParams));
254
+ } catch (error) {
255
+ const response = validationErrorResponse(error);
256
+ if (response) return response;
257
+ throw error;
258
+ }
167
259
  }
168
260
  }
169
261
 
170
- // Pass the data into $props
171
- // TODO: Promise.all
172
- // NOTE: We're loading data from top to bottom, overriding as we go, and I'm not sure if this is the best way to go
173
- let data = {};
174
- if (handler.layouts) {
175
- for (let layout of handler.layouts) {
176
- const layoutEndPoint: PageEndPoint | undefined = (await layout.endPoint())?.default;
177
- const layoutServerEndPoint: PageServerEndPoint | undefined =
178
- layout.serverEndPoint && (await layout.serverEndPoint())?.default;
179
- const layoutResponse = await loadClientAndServerData(
180
- url,
181
- params,
182
- serverParams,
183
- data,
184
- layoutEndPoint,
185
- layoutServerEndPoint,
186
- );
187
- if (layoutResponse?.ok === false) {
188
- return layoutResponse;
262
+ // Maybe hit the server hooks
263
+ const serverParams = buildServerParams(ev, url, params, query ? { query } : {});
264
+ const serverHooks = await loadServerHooks(handler);
265
+ let entered = 0;
266
+ let enterResponse: Response | undefined = undefined;
267
+ try {
268
+ // Hooks run from the root down; a hook's enter can return a Response
269
+ // to short-circuit the request
270
+ for (; entered < serverHooks.length && !enterResponse; entered++) {
271
+ const hookResult = await serverHooks[entered].enter?.(serverParams);
272
+ if (hookResult) {
273
+ enterResponse = hookResult;
189
274
  }
190
275
  }
191
- }
192
- let endPointResponse = await loadClientAndServerData(
193
- url,
194
- params,
195
- serverParams,
196
- data,
197
- clientEndPoint,
198
- serverEndPoint,
199
- );
200
- if (endPointResponse?.ok === false) {
201
- return endPointResponse;
202
- }
203
- let $props: Record<string, any> = { data };
204
-
205
- let styles = "";
206
-
207
- // If there are layouts, work our way upwards, pushing each component into
208
- // the default slot of its parent
209
- // TODO: Also handle layout server data
210
- // TODO: There's probably a nicer way to do this with reducers or something
211
- let component = clientEndPoint.component as ServerComponent;
212
- let slots: Record<string, ServerSlotRender> | undefined = undefined;
213
- if (handler.layouts) {
214
- let slotFunctions: ServerSlotRender[] = [];
215
- slotFunctions[handler.layouts.length] = (_, $context) => {
216
- let { body, head } = (clientEndPoint.component as ServerComponent)($props, $context);
217
- styles += head;
218
- return body;
219
- };
220
- for (let i = handler.layouts.length - 1; i >= 0; i--) {
221
- const layoutEndPoint: PageEndPoint | undefined = (await handler.layouts[i].endPoint())
222
- ?.default;
223
- if (layoutEndPoint?.component) {
224
- if (i === 0) {
225
- component = layoutEndPoint.component as ServerComponent;
226
- slots = { _: slotFunctions[i + 1] };
227
- } else {
228
- slotFunctions[i] = (_, $context) => {
229
- let { body, head } = (layoutEndPoint.component as ServerComponent)($props, $context, {
230
- _: slotFunctions[i + 1],
231
- });
232
- styles += head;
233
- return body;
234
- };
276
+ if (enterResponse) {
277
+ return enterResponse;
278
+ }
279
+
280
+ // Pass the data into $props
281
+ // TODO: Promise.all
282
+ // NOTE: We're loading data from top to bottom, overriding as we go, and I'm not sure if this is the best way to go
283
+ let data = {};
284
+ if (handler.layouts) {
285
+ for (let layout of handler.layouts) {
286
+ const layoutEndPoint: PageEndPoint | undefined = (await layout.endPoint())?.default;
287
+ const layoutServerEndPoint: PageServerEndPoint | undefined =
288
+ layout.serverEndPoint && (await layout.serverEndPoint())?.default;
289
+ const layoutResponse = await loadClientAndServerData(
290
+ url,
291
+ params,
292
+ serverParams,
293
+ data,
294
+ layoutEndPoint,
295
+ layoutServerEndPoint,
296
+ );
297
+ if (layoutResponse?.ok === false) {
298
+ return layoutResponse;
299
+ }
300
+ }
301
+ }
302
+ let endPointResponse = await loadClientAndServerData(
303
+ url,
304
+ params,
305
+ serverParams,
306
+ data,
307
+ clientEndPoint,
308
+ serverEndPoint,
309
+ );
310
+ if (endPointResponse?.ok === false) {
311
+ return endPointResponse;
312
+ }
313
+ let $props: Record<string, any> = { data };
314
+
315
+ let styles = "";
316
+
317
+ // If there are layouts, work our way upwards, pushing each component into
318
+ // the default slot of its parent
319
+ // TODO: Also handle layout server data
320
+ // TODO: There's probably a nicer way to do this with reducers or something
321
+ let component = clientEndPoint.component as ServerComponent;
322
+ let slots: Record<string, ServerSlotRender> | undefined = undefined;
323
+ if (handler.layouts) {
324
+ let slotFunctions: ServerSlotRender[] = [];
325
+ slotFunctions[handler.layouts.length] = (_, $context) => {
326
+ let { body, head } = (clientEndPoint.component as ServerComponent)($props, $context);
327
+ styles += head;
328
+ return body;
329
+ };
330
+ for (let i = handler.layouts.length - 1; i >= 0; i--) {
331
+ const layoutEndPoint: PageEndPoint | undefined = (await handler.layouts[i].endPoint())
332
+ ?.default;
333
+ if (layoutEndPoint?.component) {
334
+ if (i === 0) {
335
+ component = layoutEndPoint.component as ServerComponent;
336
+ slots = { _: slotFunctions[i + 1] };
337
+ } else {
338
+ slotFunctions[i] = (_, $context) => {
339
+ let { body, head } = (layoutEndPoint.component as ServerComponent)($props, $context, {
340
+ _: slotFunctions[i + 1],
341
+ });
342
+ styles += head;
343
+ return body;
344
+ };
345
+ }
235
346
  }
236
347
  }
237
348
  }
238
- }
239
349
 
240
- let html;
241
- try {
242
- let { body, head } = component($props, undefined, slots);
243
- styles += head;
244
- html = template.replace("%COMPONENT_BODY%", body).replace("%COMPONENT_HEAD", styles);
245
- } catch (error) {
246
- // TODO: Show a proper Error component
247
- html = '<span style="color: red">Script syntax error</span><p>' + error + "</p>";
248
- console.log(error);
249
- }
350
+ let html;
351
+ try {
352
+ let { body, head } = component($props, undefined, slots);
353
+ styles += head;
354
+ html = template.replace("%COMPONENT_BODY%", body).replace("%COMPONENT_HEAD%", styles);
355
+ } catch (error) {
356
+ // TODO: Show a proper Error component
357
+ html = '<span style="color: red">Script syntax error</span><p>' + error + "</p>";
358
+ console.log(error);
359
+ }
250
360
 
251
- return new Response(html, {
252
- status: 200,
253
- headers: {
254
- "Content-Type": "text/html",
255
- },
256
- });
361
+ return new Response(html, {
362
+ status: 200,
363
+ headers: {
364
+ "Content-Type": "text/html",
365
+ },
366
+ });
367
+ } finally {
368
+ // Run exit hooks in reverse order for hooks that were entered
369
+ while (entered > 0) {
370
+ entered--;
371
+ await serverHooks[entered].exit?.(serverParams);
372
+ }
373
+ }
257
374
  }
258
375
 
259
376
  async function runAction(
260
377
  ev: ServerEvent,
261
378
  url: URL,
262
379
  handler: RouteHandler,
263
- params: Record<string, string>,
380
+ serverEndPoint: PageServerEndPoint | undefined,
381
+ params: Record<string, any>,
264
382
  query: URLSearchParams,
265
383
  ) {
266
384
  const actionName = (Array.from(query.keys())[0] || "default").replace(/^\//, "");
267
- const serverEndPoint: PageServerEndPoint | undefined =
268
- handler.serverEndPoint && (await handler.serverEndPoint()).default;
269
385
  if (serverEndPoint?.actions) {
270
386
  const action = serverEndPoint.actions[actionName];
271
387
  if (action) {
388
+ if (actionName === "load" || actionName === "params") {
389
+ // These names are reserved in the endpoint's schema map: `load`
390
+ // validates the query string and `params` the route params, so
391
+ // an action with one of these names would get the wrong schema
392
+ // applied to its form data
393
+ throw new Error(
394
+ `The action name "${actionName}" is reserved. ` +
395
+ `Rename the action (and its schema key, if any), e.g. to "save".`,
396
+ );
397
+ }
272
398
  // TODO: form.errors etc
273
- const serverParams = buildServerParams(ev, url, params);
399
+ // Route params come from the URL, so a failed validation means the
400
+ // resource doesn't exist
401
+ try {
402
+ params = await validateEndpointParams(serverEndPoint, params);
403
+ } catch (error) {
404
+ if (error instanceof ValidationError) return notFound();
405
+ throw error;
406
+ }
274
407
 
275
- if (handler.serverHook) {
276
- const serverHook: ServerHook | undefined = (await handler.serverHook()).default;
277
- if (serverHook?.handle) {
278
- await serverHook.handle(serverParams);
408
+ let result: Response | undefined | void;
409
+ try {
410
+ // If the endpoint declares a schema for this action, validate the
411
+ // submitted form data up front and reject with 422 if it fails
412
+ const schema = endpointSchema(serverEndPoint, actionName);
413
+ const form = schema
414
+ ? await validate(schema, await formDataToRecord(ev.request))
415
+ : undefined;
416
+
417
+ const serverParams = buildServerParams(ev, url, params, form ? { form } : {});
418
+
419
+ const serverHooks = await loadServerHooks(handler);
420
+ let entered = 0;
421
+ let enterResponse: Response | undefined = undefined;
422
+ try {
423
+ // Hooks run from the root down; a hook's enter can return a
424
+ // Response to short-circuit the action
425
+ for (; entered < serverHooks.length && !enterResponse; entered++) {
426
+ const hookResult = await serverHooks[entered].enter?.(serverParams);
427
+ if (hookResult) {
428
+ enterResponse = hookResult;
429
+ }
430
+ }
431
+
432
+ result = enterResponse || (await action(serverParams));
433
+ } finally {
434
+ // Run exit hooks in reverse order for hooks that were entered
435
+ while (entered > 0) {
436
+ entered--;
437
+ await serverHooks[entered].exit?.(serverParams);
438
+ }
279
439
  }
440
+ } catch (error) {
441
+ const response = validationErrorResponse(error);
442
+ if (response) return response;
443
+ throw error;
280
444
  }
281
445
 
282
- const result = await action(serverParams); // <--
283
-
284
446
  // If there was no response returned from the action (such as errors
285
447
  // or a redirect), reload the page by redirecting
286
448
  return result || seeOther(url.pathname);
@@ -292,14 +454,14 @@ async function runAction(
292
454
 
293
455
  async function loadClientAndServerData(
294
456
  url: URL,
295
- params: Record<string, string>,
457
+ params: Record<string, any>,
296
458
  serverParams: ServerLoadEvent,
297
459
  data: Record<string, any>,
298
460
  clientEndPoint?: PageEndPoint,
299
461
  serverEndPoint?: PageServerEndPoint,
300
462
  ) {
301
463
  if (clientEndPoint?.load) {
302
- const clientParams = buildClientParams(url, data, params);
464
+ const clientParams = buildClientParams(url, params, data);
303
465
  const clientResponse = await clientEndPoint.load(clientParams);
304
466
  if (clientResponse) {
305
467
  if (clientResponse.ok) {
@@ -325,7 +487,7 @@ async function loadClientAndServerData(
325
487
  }
326
488
  }
327
489
 
328
- function buildClientParams(url: URL, params: Record<string, string>, data: Record<string, any>) {
490
+ function buildClientParams(url: URL, params: Record<string, any>, data: Record<string, any>) {
329
491
  return {
330
492
  url,
331
493
  params,
@@ -333,16 +495,42 @@ function buildClientParams(url: URL, params: Record<string, string>, data: Recor
333
495
  };
334
496
  }
335
497
 
498
+ /**
499
+ * Loads the route's server hooks, from the root down
500
+ */
501
+ async function loadServerHooks(handler: RouteHandler): Promise<ServerHook[]> {
502
+ const hooks: ServerHook[] = [];
503
+ if (handler.serverHooks) {
504
+ for (let serverHook of handler.serverHooks) {
505
+ hooks.push((await serverHook()).default);
506
+ }
507
+ }
508
+ return hooks;
509
+ }
510
+
336
511
  function buildServerParams(
337
512
  ev: ServerEvent,
338
513
  url: URL,
339
- params: Record<string, string>,
514
+ params: Record<string, any>,
515
+ values: { json?: unknown; form?: unknown; query?: unknown } = {},
340
516
  ): ServerLoadEvent {
341
517
  return {
342
518
  url,
343
519
  params,
344
520
  appData: {},
345
521
  request: ev.request,
522
+ // The inputs may have been validated already, in which case the request
523
+ // body has been consumed and the parsed values are returned instead.
524
+ // Their types come from the endpoint's schemas, so they are loosely
525
+ // typed here
526
+ json: (): Promise<any> =>
527
+ values.json !== undefined ? Promise.resolve(values.json) : ev.json(),
528
+ form: (): Promise<any> =>
529
+ values.form !== undefined ? Promise.resolve(values.form) : formDataToRecord(ev.request),
530
+ query: (): Promise<any> =>
531
+ values.query !== undefined
532
+ ? Promise.resolve(values.query)
533
+ : Promise.resolve(searchParamsToRecord(url.searchParams)),
346
534
  cookies: ev.cookies,
347
535
  headers: ev.headers,
348
536
  adapter: ev.adapter,
@@ -1,8 +1,20 @@
1
+ import type { Plugin } from "vite";
1
2
  import Server from "../server/Server";
2
3
  import Site from "../site/Site";
3
4
 
4
5
  export default interface Adapter {
5
6
  prebuild?: (site: Site) => Promise<void> | void;
6
7
  postbuild?: (site: Site) => Promise<void> | void;
8
+ /**
9
+ * Vite plugin(s) used to drive dev mode. The adapter owns where server code
10
+ * runs (e.g. the default Node runtime, or workerd via miniflare) and how
11
+ * requests reach it. When omitted, the framework's default Node-runtime dev
12
+ * plugin is used.
13
+ */
14
+ dev?: (site: Site) => Plugin | Plugin[] | void;
15
+ /**
16
+ * Serve the built output (preview). Receives a torpor Server configured to
17
+ * load the built server entry.
18
+ */
7
19
  serve: (server: Server, site: Site) => void;
8
20
  }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The JSON-serialized form of a type: what a value looks like after a
3
+ * JSON.stringify / JSON.parse round trip. Dates become strings, functions and
4
+ * undefined properties are dropped.
5
+ */
6
+ export type Jsonify<T> = T extends string | number | boolean | null
7
+ ? T
8
+ : T extends undefined | ((...args: any[]) => any)
9
+ ? never
10
+ : T extends Date
11
+ ? string
12
+ : T extends Array<infer Item>
13
+ ? Array<Jsonify<Item>>
14
+ : T extends object
15
+ ? {
16
+ [Key in keyof T as T[Key] extends undefined | ((...args: any[]) => any)
17
+ ? never
18
+ : Key]: Jsonify<T[Key]>;
19
+ }
20
+ : T;