@torpor/build 1.0.0 → 1.0.2

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 (174) hide show
  1. package/dist/{flattenHeaders-C_YYLdOq.mjs → Server-BnjLJleZ.mjs} +2 -12
  2. package/dist/Server-BnjLJleZ.mjs.map +1 -0
  3. package/dist/{Server-C_IKav_e.d.mts → Server-Cler2w5O.d.mts} +3 -48
  4. package/dist/Server-Cler2w5O.d.mts.map +1 -0
  5. package/dist/ServerEvent-CTx3Q-hk.d.mts +48 -0
  6. package/dist/ServerEvent-CTx3Q-hk.d.mts.map +1 -0
  7. package/dist/{Site-CMa153FA.d.mts → Site-CE0_o5Ci.d.mts} +3 -2
  8. package/dist/Site-CE0_o5Ci.d.mts.map +1 -0
  9. package/dist/bin/index.js +1 -1
  10. package/dist/clientEntry.d.mts +1 -0
  11. package/dist/clientEntry.mjs +59 -0
  12. package/dist/clientEntry.mjs.map +1 -0
  13. package/dist/clientEntryDev.d.mts +1 -0
  14. package/dist/clientEntryDev.mjs +7 -0
  15. package/dist/clientEntryDev.mjs.map +1 -0
  16. package/dist/dev.d.mts +3 -0
  17. package/dist/dev.mjs +3 -0
  18. package/dist/endpoint-EULRGCKA.mjs +108 -0
  19. package/dist/endpoint-EULRGCKA.mjs.map +1 -0
  20. package/dist/flattenHeaders-DM5qsSqO.mjs +13 -0
  21. package/dist/flattenHeaders-DM5qsSqO.mjs.map +1 -0
  22. package/dist/index.d.mts +1 -1
  23. package/dist/load-BntzvmwL.mjs +228 -0
  24. package/dist/load-BntzvmwL.mjs.map +1 -0
  25. package/dist/nav.mjs +1 -225
  26. package/dist/nav.mjs.map +1 -1
  27. package/dist/openapi.d.mts +1 -1
  28. package/dist/run.d.mts +1 -1
  29. package/dist/run.d.mts.map +1 -1
  30. package/dist/run.mjs +1 -1
  31. package/dist/{runOpenApi-BUi1RH1l.mjs → runOpenApi-C8eI2Fh0.mjs} +54 -16
  32. package/dist/runOpenApi-C8eI2Fh0.mjs.map +1 -0
  33. package/dist/server/Server.d.mts +2 -0
  34. package/dist/server/Server.mjs +2 -0
  35. package/dist/server.d.mts +2 -1
  36. package/dist/server.d.mts.map +1 -1
  37. package/dist/server.mjs +2 -1
  38. package/dist/server.mjs.map +1 -1
  39. package/dist/serverEntry-1qY3wZig.mjs +351 -0
  40. package/dist/serverEntry-1qY3wZig.mjs.map +1 -0
  41. package/dist/serverEntry-BAO8m03V.d.mts +6 -0
  42. package/dist/serverEntry-BAO8m03V.d.mts.map +1 -0
  43. package/dist/serverEntry.d.mts +2 -0
  44. package/dist/serverEntry.mjs +2 -0
  45. package/dist/test.d.mts +2 -2
  46. package/dist/test.mjs +2 -104
  47. package/dist/test.mjs.map +1 -1
  48. package/package.json +9 -8
  49. package/dist/Server-C_IKav_e.d.mts.map +0 -1
  50. package/dist/Site-CMa153FA.d.mts.map +0 -1
  51. package/dist/flattenHeaders-C_YYLdOq.mjs.map +0 -1
  52. package/dist/runOpenApi-BUi1RH1l.mjs.map +0 -1
  53. package/src/bin/index.ts +0 -22
  54. package/src/dev.ts +0 -10
  55. package/src/form/formDataToRecord.ts +0 -23
  56. package/src/form/readForm.ts +0 -71
  57. package/src/form.ts +0 -4
  58. package/src/index.ts +0 -82
  59. package/src/nav/api.test-d.ts +0 -79
  60. package/src/nav/api.ts +0 -104
  61. package/src/nav/formSubmit.ts +0 -70
  62. package/src/nav/load.ts +0 -9
  63. package/src/nav/loadData.ts +0 -134
  64. package/src/nav/navigate.ts +0 -189
  65. package/src/nav/reload.ts +0 -9
  66. package/src/nav/route.ts +0 -38
  67. package/src/nav.ts +0 -6
  68. package/src/openapi/docsHtml.ts +0 -25
  69. package/src/openapi/document.ts +0 -154
  70. package/src/openapi/plugin.ts +0 -78
  71. package/src/openapi/types.ts +0 -82
  72. package/src/openapi.ts +0 -14
  73. package/src/response/TypedResponse.ts +0 -17
  74. package/src/response/badRequest.ts +0 -31
  75. package/src/response/created.ts +0 -31
  76. package/src/response/forbidden.ts +0 -21
  77. package/src/response/found.ts +0 -30
  78. package/src/response/methodNotAllowed.ts +0 -18
  79. package/src/response/movedPermanently.ts +0 -25
  80. package/src/response/notFound.ts +0 -24
  81. package/src/response/notModified.ts +0 -34
  82. package/src/response/ok.ts +0 -38
  83. package/src/response/permanentRedirect.ts +0 -30
  84. package/src/response/response.ts +0 -22
  85. package/src/response/seeOther.ts +0 -19
  86. package/src/response/serverError.ts +0 -24
  87. package/src/response/temporaryRedirect.ts +0 -31
  88. package/src/response/transfer.ts +0 -14
  89. package/src/response/unauthorized.ts +0 -21
  90. package/src/response/unprocessable.ts +0 -30
  91. package/src/response.ts +0 -37
  92. package/src/run/depCache.ts +0 -175
  93. package/src/run/devPlugin.ts +0 -128
  94. package/src/run/prepareTemplate.ts +0 -42
  95. package/src/run/run.ts +0 -86
  96. package/src/run/runBuild.ts +0 -128
  97. package/src/run/runDev.ts +0 -177
  98. package/src/run/runOpenApi.ts +0 -52
  99. package/src/run/runPreview.ts +0 -84
  100. package/src/run/staleTorpCopies.ts +0 -100
  101. package/src/run.ts +0 -7
  102. package/src/schema.ts +0 -7
  103. package/src/server/CookieHelper.ts +0 -46
  104. package/src/server/HeaderHelper.ts +0 -25
  105. package/src/server/Server.ts +0 -168
  106. package/src/server/ServerEvent.ts +0 -62
  107. package/src/server/connect/connectMiddleware.ts +0 -85
  108. package/src/server/connect/flattenHeaders.ts +0 -22
  109. package/src/server/connect/nodeMessageToNodeResponse.ts +0 -89
  110. package/src/server/connect/requestToNodeMessage.ts +0 -29
  111. package/src/server/contentType.ts +0 -85
  112. package/src/server/types/HttpMethod.ts +0 -12
  113. package/src/server/types/MiddlewareFunction.ts +0 -22
  114. package/src/server/types/ServerFunction.ts +0 -7
  115. package/src/server.ts +0 -7
  116. package/src/site/Router.ts +0 -175
  117. package/src/site/Site.ts +0 -388
  118. package/src/site/checkLayoutSlots.ts +0 -114
  119. package/src/site/checkRoutes.ts +0 -435
  120. package/src/site/clientEntry.ts +0 -121
  121. package/src/site/clientEntryDev.ts +0 -3
  122. package/src/site/defaultAdapter.ts +0 -11
  123. package/src/site/layoutSlots.ts +0 -53
  124. package/src/site/manifest.ts +0 -173
  125. package/src/site/serverEntry.ts +0 -648
  126. package/src/state/$page.ts +0 -25
  127. package/src/state/$serverPage.ts +0 -22
  128. package/src/state/client.ts +0 -15
  129. package/src/state.ts +0 -3
  130. package/src/test/runTest.ts +0 -538
  131. package/src/test.ts +0 -3
  132. package/src/types/Adapter.ts +0 -20
  133. package/src/types/ClientState.ts +0 -8
  134. package/src/types/InternalState.ts +0 -7
  135. package/src/types/Jsonify.ts +0 -20
  136. package/src/types/LayoutHandler.ts +0 -5
  137. package/src/types/LayoutPath.ts +0 -10
  138. package/src/types/ManifestRoute.ts +0 -6
  139. package/src/types/PageData.test-d.ts +0 -136
  140. package/src/types/PageData.ts +0 -35
  141. package/src/types/PageEndPoint.ts +0 -55
  142. package/src/types/PageForm.test-d.ts +0 -78
  143. package/src/types/PageForm.ts +0 -29
  144. package/src/types/PageLoadEvent.ts +0 -26
  145. package/src/types/PageLoadReturn.ts +0 -13
  146. package/src/types/PageProps.ts +0 -14
  147. package/src/types/PageServerAction.ts +0 -13
  148. package/src/types/PageServerEndPoint.test-d.ts +0 -105
  149. package/src/types/PageServerEndPoint.ts +0 -110
  150. package/src/types/PageServerLoad.ts +0 -15
  151. package/src/types/PageState.ts +0 -8
  152. package/src/types/ParseRouteParams.test-d.ts +0 -84
  153. package/src/types/ParseRouteParams.ts +0 -64
  154. package/src/types/Route.ts +0 -30
  155. package/src/types/RouteHandler.ts +0 -23
  156. package/src/types/RouteLayoutHandler.ts +0 -5
  157. package/src/types/RouteMatchResult.ts +0 -7
  158. package/src/types/RouteType.ts +0 -19
  159. package/src/types/ServerEndPoint.test-d.ts +0 -77
  160. package/src/types/ServerEndPoint.ts +0 -169
  161. package/src/types/ServerHook.ts +0 -18
  162. package/src/types/ServerLoadEvent.ts +0 -82
  163. package/src/types/ServerRequest.ts +0 -15
  164. package/src/types/SitePlugin.ts +0 -24
  165. package/src/types/StandardSchema.ts +0 -109
  166. package/src/utils/pathToRegex.ts +0 -17
  167. package/src/utils/pathTrie.ts +0 -182
  168. package/src/utils/searchParamsToRecord.ts +0 -18
  169. package/src/utils/torporPackages.ts +0 -150
  170. package/src/utils/tsconfigAliases.ts +0 -90
  171. package/src/validation/ValidationError.ts +0 -18
  172. package/src/validation/endpoint.ts +0 -83
  173. package/src/validation/validate.ts +0 -26
  174. package/src/vite-env.d.ts +0 -2
@@ -1,648 +0,0 @@
1
- import manifest from "@torpor/build/manifest";
2
- import { type ServerComponent, type ServerSlotRender } from "@torpor/view/ssr";
3
- import formDataToRecord from "../form/formDataToRecord.ts";
4
- import notFound from "../response/notFound.ts";
5
- import ok from "../response/ok.ts";
6
- import seeOther from "../response/seeOther.ts";
7
- import ServerEvent from "../server/ServerEvent.ts";
8
- import $page from "../state/$serverPage.ts";
9
- import type PageEndPoint from "../types/PageEndPoint.ts";
10
- import type PageServerEndPoint from "../types/PageServerEndPoint.ts";
11
- import type RouteHandler from "../types/RouteHandler.ts";
12
- import type ServerRequest from "../types/ServerRequest.ts";
13
- import {
14
- ERROR_ROUTE,
15
- HOOK_ROUTE,
16
- HOOK_SERVER_ROUTE,
17
- LAYOUT_ROUTE,
18
- LAYOUT_SERVER_ROUTE,
19
- PAGE_ROUTE,
20
- PAGE_SERVER_ROUTE,
21
- SERVER_ROUTE,
22
- } from "../types/RouteType";
23
- import type ServerEndPoint from "../types/ServerEndPoint.ts";
24
- import type ServerHook from "../types/ServerHook.ts";
25
- import type ServerLoadEvent from "../types/ServerLoadEvent.ts";
26
- import searchParamsToRecord from "../utils/searchParamsToRecord.ts";
27
- import validate from "../validation/validate.ts";
28
- import ValidationError from "../validation/ValidationError.ts";
29
- import {
30
- endpointSchema,
31
- validateEndpointParams,
32
- validationErrorResponse,
33
- } from "../validation/endpoint.ts";
34
- import { findMissingSlotLayout, warnMissingSlotContent } from "./layoutSlots.ts";
35
- import Router from "./Router.ts";
36
-
37
- // Build the router from the Site object created by the user
38
- const router = new Router();
39
- router.addPages(manifest.routes);
40
-
41
- //console.log(`routes:\n ${router.routes.map((r) => r.path).join("\n ")}`);
42
-
43
- export async function load(ev: ServerEvent, template?: string): Promise<Response> {
44
- const url = ev.url;
45
- const path = url.pathname;
46
- const query = url.searchParams;
47
-
48
- //console.log(`handling ${ev.request.method} for '${path}'${query.size ? ` with ${query}` : ""}`);
49
-
50
- let route = router.match(path, query);
51
- if (!route) {
52
- // If the route wasn't found, the user may be posting a form to a
53
- // +page.server.ts route that doesn't have a corresponding +page.ts
54
- // route, which is allowed, so check for that as well
55
- if (ev.request.method === "POST" && !path.endsWith("/~server")) {
56
- const serverPath =
57
- (path.endsWith("/") ? path.substring(0, path.length - 1) : path) + "/~server";
58
- route = router.match(serverPath, query);
59
- }
60
- }
61
- if (!route) {
62
- return handleResponse(notFound());
63
- }
64
- const handler = route.handler;
65
- const params = route.params || {};
66
-
67
- // Update $page before building the components
68
- $page.url = url;
69
- if (path.endsWith("/_error")) {
70
- $page.status = parseInt(query.get("status") ?? "404");
71
- $page.error = { message: query.get("message") ?? "" };
72
- } else {
73
- $page.status = 200;
74
- }
75
-
76
- // TODO: Hit the server hook out here
77
- // We could maybe set data loading up as middleware on a route??????
78
-
79
- switch (handler.type) {
80
- case PAGE_ROUTE:
81
- case LAYOUT_ROUTE: {
82
- // It's a /+page.ts or /_layout.ts endpoint
83
- if (ev.request.method === "GET") {
84
- return handleResponse(await loadView(ev, url, handler, params, template));
85
- } else if (ev.request.method === "POST") {
86
- // The server end point comes from the sibling /+page.server.ts, if applicable
87
- const serverEndPoint: PageServerEndPoint | undefined =
88
- handler.serverEndPoint && (await handler.serverEndPoint()).default;
89
- return handleResponse(
90
- await runAction(ev, url, handler, serverEndPoint, params, query, template),
91
- true,
92
- );
93
- }
94
- break;
95
- }
96
- case PAGE_SERVER_ROUTE:
97
- case LAYOUT_SERVER_ROUTE: {
98
- // It's a /+page.server.ts or /_layout.server.ts endpoint
99
- if (ev.request.method === "GET") {
100
- return await loadData(ev, url, handler, "load", params);
101
- } else if (ev.request.method === "POST") {
102
- const serverEndPoint: PageServerEndPoint | undefined = (await resolveModule(handler))
103
- .default;
104
- return handleResponse(
105
- await runAction(ev, url, handler, serverEndPoint, params, query, template),
106
- true,
107
- );
108
- }
109
- break;
110
- }
111
- case SERVER_ROUTE: {
112
- // It's a /+server.ts endpoint
113
- const functionName = ev.request.method.toLowerCase().replace("delete", "del");
114
- return await loadData(ev, url, handler, functionName, params);
115
- }
116
- case HOOK_ROUTE:
117
- case HOOK_SERVER_ROUTE: {
118
- break;
119
- }
120
- case ERROR_ROUTE: {
121
- return await loadView(ev, url, handler, params, template);
122
- }
123
- }
124
-
125
- return notFound();
126
- }
127
-
128
- /**
129
- * Resolves the route's endpoint module, caching it on the handler so warm
130
- * requests don't pay for the loader promise.
131
- */
132
- async function resolveModule(handler: RouteHandler): Promise<any> {
133
- return (handler.resolvedModule ??= await (handler.modulePromise ??= handler.endPoint()));
134
- }
135
-
136
- async function loadServerHooks(handler: RouteHandler): Promise<ServerHook[]> {
137
- if (!handler.serverHooks) return [];
138
- // Resolve each hook module once and cache the result on the handler
139
- return (handler.resolvedHooks ??= Promise.all(
140
- handler.serverHooks.map(async (load) => (await load()).default),
141
- ));
142
- }
143
-
144
- async function handleResponse(response: Response, fromForm = false): Promise<Response> {
145
- $page.status = response.status;
146
-
147
- // Success codes and redirect codes are acceptable
148
- if (response.status >= 200 && response.status <= 399) {
149
- return response;
150
- }
151
-
152
- // 4xx error codes are acceptable from form actions
153
- if (fromForm && response.status >= 400 && response.status <= 499) {
154
- return response;
155
- }
156
-
157
- // It's an error, so redirect to the error page
158
- // We're just pushing the status and message in the URL, but maybe there's a
159
- // more sophisticated way to do this?
160
- // TODO: Should be returning loadView rather than redirecting
161
- // TODO: Should be returning the NEAREST error page to this path, including layouts
162
- let params = new URLSearchParams();
163
- params.append("status", $page.status.toString());
164
- let message = await response.text();
165
- if (response.headers.get("Content-Type")?.includes("application/json")) {
166
- const data = JSON.parse(message);
167
- message = data.message ?? message;
168
- }
169
- if (message) {
170
- if ($page.error) {
171
- $page.error.message = message;
172
- } else {
173
- $page.error = { message };
174
- }
175
- params.append("message", message);
176
- }
177
- return seeOther(`/_error?${params.toString()}`);
178
- }
179
-
180
- async function loadData(
181
- ev: ServerEvent,
182
- url: URL,
183
- handler: RouteHandler,
184
- functionName: string,
185
- params: Record<string, any>,
186
- ) {
187
- // The endpoint module is cached on the handler, so once warm this is a
188
- // plain property read
189
- const mod = handler.resolvedModule ?? (await resolveModule(handler));
190
- const serverEndPoint: ServerEndPoint | undefined = mod?.default;
191
- // `functionName` is computed from the request method, so the handlers are
192
- // accessed through a loose record
193
- const handlerFn = (
194
- serverEndPoint as unknown as Record<string, ServerRequest | undefined> | undefined
195
- )?.[functionName];
196
- if (!serverEndPoint || !handlerFn) {
197
- return notFound();
198
- }
199
-
200
- // Route params come from the URL, so a failed validation means the
201
- // resource doesn't exist. Only endpoints declaring a params schema pay
202
- // for validation
203
- const paramsSchema = endpointSchema(serverEndPoint, "params");
204
- if (paramsSchema) {
205
- try {
206
- params = (await validate(paramsSchema, params)) as Record<string, any>;
207
- } catch (error) {
208
- if (error instanceof ValidationError) return notFound();
209
- throw error;
210
- }
211
- }
212
-
213
- try {
214
- // If the endpoint declares a schema for this handler, validate its
215
- // input up front and reject with 422 if it fails. get/head/load
216
- // schemas validate the query string; other schemas validate the json
217
- // body. Endpoints without a schema skip straight to the handler
218
- const schema = endpointSchema(serverEndPoint, functionName);
219
- const values = schema
220
- ? functionName === "get" || functionName === "head" || functionName === "load"
221
- ? { query: await validate(schema, searchParamsToRecord(url.searchParams)) }
222
- : { json: await validate(schema, await ev.json()) }
223
- : {};
224
-
225
- const serverParams = buildServerParams(ev, url, params, values);
226
-
227
- // Hooks are resolved once per handler; routes without hooks don't
228
- // await anything here
229
- const hooks = handler.serverHooks ? await loadServerHooks(handler) : [];
230
-
231
- let entered = 0;
232
- let enterResponse: Response | undefined = undefined;
233
- try {
234
- // Hooks run from the root down; a hook's enter can return a
235
- // Response to short-circuit the request
236
- for (; entered < hooks.length && !enterResponse; entered++) {
237
- const hookResult = await hooks[entered].enter?.(serverParams);
238
- if (hookResult) {
239
- enterResponse = hookResult;
240
- }
241
- }
242
-
243
- // If there was no response returned from load (such as errors or a
244
- // redirect), send an ok response
245
- return (await handlerFn(serverParams)) || ok();
246
- } finally {
247
- // Run exit hooks in reverse order for hooks that were entered
248
- while (entered > 0) {
249
- entered--;
250
- await hooks[entered].exit?.(serverParams);
251
- }
252
- }
253
- } catch (error) {
254
- const response = validationErrorResponse(error);
255
- if (response) return response;
256
- throw error;
257
- }
258
- }
259
-
260
- async function loadView(
261
- ev: ServerEvent,
262
- url: URL,
263
- handler: RouteHandler,
264
- params: Record<string, any>,
265
- template: string | undefined,
266
- formStatus?: number,
267
- form?: Record<string, string | number>,
268
- skipHook?: boolean,
269
- ) {
270
- // There must be a client endpoint with a component
271
- const clientEndPoint: PageEndPoint | undefined = (await resolveModule(handler)).default;
272
- if (!clientEndPoint?.component) {
273
- return notFound();
274
- }
275
-
276
- // The template comes from site.html, which endpoints-only sites don't have
277
- if (!template) {
278
- throw new Error(
279
- "Page routes require a src/site.html file, which was not found; add it, or remove page/layout routes to serve endpoints only",
280
- );
281
- }
282
-
283
- // There may be a server endpoint
284
- const serverEndPoint: PageServerEndPoint | undefined =
285
- handler.serverEndPoint && (await handler.serverEndPoint())?.default;
286
-
287
- // Validate the route params and the load query against the server
288
- // endpoint's schemas, if it declares any. Params come from the URL, so a
289
- // failed validation means the resource doesn't exist; a failed query goes
290
- // to the error page like any other load failure
291
- let query: unknown;
292
- if (serverEndPoint) {
293
- try {
294
- params = await validateEndpointParams(serverEndPoint, params);
295
- } catch (error) {
296
- if (error instanceof ValidationError) return notFound();
297
- throw error;
298
- }
299
- const loadSchema = endpointSchema(serverEndPoint, "load");
300
- if (loadSchema) {
301
- try {
302
- query = await validate(loadSchema, searchParamsToRecord(url.searchParams));
303
- } catch (error) {
304
- const response = validationErrorResponse(error);
305
- if (response) return response;
306
- throw error;
307
- }
308
- }
309
- }
310
-
311
- // Maybe hit the server hooks -- unless they already ran for this request,
312
- // which is the case when the view is re-rendered after a form action
313
- const serverParams = buildServerParams(ev, url, params, query ? { query } : {});
314
- const serverHooks = skipHook ? [] : await loadServerHooks(handler);
315
- let entered = 0;
316
- let enterResponse: Response | undefined = undefined;
317
- try {
318
- // Hooks run from the root down; a hook's enter can return a Response
319
- // to short-circuit the request
320
- for (; entered < serverHooks.length && !enterResponse; entered++) {
321
- const hookResult = await serverHooks[entered].enter?.(serverParams);
322
- if (hookResult) {
323
- enterResponse = hookResult;
324
- }
325
- }
326
- if (enterResponse) {
327
- return enterResponse;
328
- }
329
-
330
- // Pass the data into $props
331
- // TODO: Promise.all
332
- // NOTE: We're loading data from top to bottom, overriding as we go, and I'm
333
- // not sure if this is the best way to go
334
- let data = {};
335
- if (handler.layouts) {
336
- for (let layout of handler.layouts) {
337
- const layoutEndPoint: PageEndPoint | undefined = (await layout.endPoint())?.default;
338
- const layoutServerEndPoint: PageServerEndPoint | undefined =
339
- layout.serverEndPoint && (await layout.serverEndPoint())?.default;
340
- const layoutResponse = await loadClientAndServerData(
341
- url,
342
- params,
343
- serverParams,
344
- data,
345
- layoutEndPoint,
346
- layoutServerEndPoint,
347
- );
348
- if (layoutResponse?.ok === false) {
349
- return layoutResponse;
350
- }
351
- }
352
- }
353
- let endPointResponse = await loadClientAndServerData(
354
- url,
355
- params,
356
- serverParams,
357
- data,
358
- clientEndPoint,
359
- serverEndPoint,
360
- );
361
- if (endPointResponse?.ok === false) {
362
- return endPointResponse;
363
- }
364
- let $props: Record<string, any> = { data, form };
365
-
366
- let styles = "";
367
-
368
- // If there are layouts, work our way upwards, pushing each component into
369
- // the default slot of its parent
370
- // TODO: Also handle layout server data
371
- // TODO: There's probably a nicer way to do this with reducers or something
372
- let component = clientEndPoint.component as ServerComponent;
373
- let slots: Record<string, ServerSlotRender> | undefined = undefined;
374
- // Which slot levels actually ran during the render below, so a layout
375
- // that never rendered its <slot /> (silently dropping the page
376
- // content) can be reported (see findMissingSlotLayout)
377
- const invokedLevels = new Set<number>();
378
- const trackSlot = (level: number, slot: ServerSlotRender): ServerSlotRender => {
379
- return ($slot, $context) => {
380
- invokedLevels.add(level);
381
- return slot($slot, $context);
382
- };
383
- };
384
- if (handler.layouts) {
385
- let slotFunctions: ServerSlotRender[] = [];
386
- // The last slot function will render the client component
387
- slotFunctions[handler.layouts.length] = trackSlot(handler.layouts.length, (_, $context) => {
388
- let { body, head } = (clientEndPoint.component as ServerComponent)($props, $context);
389
- styles += head;
390
- return body;
391
- });
392
- for (let i = handler.layouts.length - 1; i >= 0; i--) {
393
- const layoutEndPoint: PageEndPoint | undefined = (await handler.layouts[i].endPoint())
394
- ?.default;
395
- if (layoutEndPoint?.component) {
396
- if (i === 0) {
397
- component = layoutEndPoint.component as ServerComponent;
398
- slots = { _: slotFunctions[i + 1] };
399
- } else {
400
- slotFunctions[i] = trackSlot(i, (_, $context) => {
401
- let { body, head } = (layoutEndPoint.component as ServerComponent)($props, $context, {
402
- _: slotFunctions[i + 1],
403
- });
404
- styles += head;
405
- return body;
406
- });
407
- }
408
- }
409
- }
410
- }
411
-
412
- let html;
413
- try {
414
- let { body, head } = component($props, undefined, slots);
415
-
416
- // Put the form info in a hidden input so that it can be accessed on the client
417
- if (form) {
418
- body += `\n<input type="hidden" id="t-form-data" value='${JSON.stringify(form).replaceAll("'", "\\'")}' />`;
419
- }
420
-
421
- styles += head;
422
- html = template.replace("%COMPONENT_BODY%", body).replace("%COMPONENT_HEAD%", styles);
423
- } catch (error) {
424
- // TODO: Show a proper Error component
425
- html = '<span style="color: red">Script syntax error</span><p>' + error + "</p>";
426
- console.log(error);
427
- }
428
-
429
- // A layout that never rendered <slot /> silently drops the whole page
430
- // content from the output; find and report the outermost one
431
- if (handler.layouts) {
432
- const missingLayout = findMissingSlotLayout(handler.layouts, invokedLevels);
433
- if (missingLayout !== undefined) {
434
- warnMissingSlotContent(missingLayout);
435
- }
436
- }
437
-
438
- return new Response(html, {
439
- status: formStatus ?? 200,
440
- headers: {
441
- "Content-Type": "text/html",
442
- },
443
- });
444
- } finally {
445
- // Run exit hooks in reverse order for hooks that were entered
446
- while (entered > 0) {
447
- entered--;
448
- await serverHooks[entered].exit?.(serverParams);
449
- }
450
- }
451
- }
452
-
453
- async function runAction(
454
- ev: ServerEvent,
455
- url: URL,
456
- handler: RouteHandler,
457
- serverEndPoint: PageServerEndPoint | undefined,
458
- params: Record<string, any>,
459
- query: URLSearchParams,
460
- template: string | undefined,
461
- ) {
462
- const actionName = (Array.from(query.keys())[0] || "default").replace(/^\//, "");
463
- if (serverEndPoint?.actions) {
464
- const action = serverEndPoint.actions[actionName];
465
- if (action) {
466
- if (actionName === "load" || actionName === "params") {
467
- // These names are reserved in the endpoint's schema map: `load`
468
- // validates the query string and `params` the route params, so
469
- // an action with one of these names would get the wrong schema
470
- // applied to its form data
471
- throw new Error(
472
- `The action name "${actionName}" is reserved. ` +
473
- `Rename the action (and its schema key, if any), e.g. to "save".`,
474
- );
475
- }
476
- // Route params come from the URL, so a failed validation means the
477
- // resource doesn't exist
478
- try {
479
- params = await validateEndpointParams(serverEndPoint, params);
480
- } catch (error) {
481
- if (error instanceof ValidationError) return notFound();
482
- throw error;
483
- }
484
-
485
- let result: Response | undefined | void;
486
- try {
487
- // If the endpoint declares a schema for this action, validate the
488
- // submitted form data up front and reject with 422 if it fails
489
- const schema = endpointSchema(serverEndPoint, actionName);
490
- const form = schema
491
- ? await validate(schema, await formDataToRecord(ev.request))
492
- : undefined;
493
-
494
- const serverParams = buildServerParams(ev, url, params, form ? { form } : {});
495
-
496
- const serverHooks = await loadServerHooks(handler);
497
- let entered = 0;
498
- let enterResponse: Response | undefined = undefined;
499
- try {
500
- // Hooks run from the root down; a hook's enter can return a
501
- // Response to short-circuit the action
502
- for (; entered < serverHooks.length && !enterResponse; entered++) {
503
- const hookResult = await serverHooks[entered].enter?.(serverParams);
504
- if (hookResult) {
505
- enterResponse = hookResult;
506
- }
507
- }
508
-
509
- result = enterResponse || (await action(serverParams));
510
- } finally {
511
- // Run exit hooks in reverse order for hooks that were entered
512
- while (entered > 0) {
513
- entered--;
514
- await serverHooks[entered].exit?.(serverParams);
515
- }
516
- }
517
- } catch (error) {
518
- const response = validationErrorResponse(error);
519
- if (response) return response;
520
- throw error;
521
- }
522
-
523
- if (ev.request.headers.has("X-Torpor-Form-Submit")) {
524
- // If the form was submitted from javascript, just return the
525
- // response and it will be handled on the client
526
- // TODO: Should we make the user return a response??
527
- result ??= ok();
528
-
529
- // If it's a redirect, we need to stop the browser handling it
530
- if (result.status >= 300 && result.status <= 399) {
531
- const location = result.headers.get("Location") ?? "";
532
- result = ok();
533
- result.headers.set("X-Torpor-Form-Redirect", "");
534
- result.headers.set("Location", location);
535
- }
536
-
537
- return result;
538
- } else if (
539
- !result ||
540
- (result.status >= 200 && result.status <= 299) ||
541
- (result.status >= 400 && result.status <= 499)
542
- ) {
543
- // If the form was submitted without javascript, and there was
544
- // no result, an ok result, or a 4xx error, re-render the view
545
- // with the form result. The server hook already ran around the
546
- // action, so it is not run again here
547
- // Keep the query string, so that loads validating the query see
548
- // the same values as the original GET
549
- const newUrl = new URL(url.pathname + url.search, url);
550
- const formStatus = result?.status;
551
- let formMessage = await result?.text();
552
- if (formMessage && result?.headers.get("Content-Type")?.includes("application/json")) {
553
- $page.form = JSON.parse(formMessage);
554
- formMessage = undefined;
555
- } else {
556
- $page.form = {};
557
- }
558
- // Set status and message if they haven't already been set,
559
- // which means that you can just return e.g. forbidden() or
560
- // forbidden("no!") and there will be something you can work
561
- // with in your component, or you can set a completely custom
562
- // object with whatever info you need
563
- $page.form!.status ??= formStatus ?? 0;
564
- $page.form!.message ??= formMessage ?? "";
565
- return await loadView(ev, newUrl, handler, params, template, formStatus, $page.form, true);
566
- } else {
567
- // If the form was submitted without javascript and there was a
568
- // redirect or server error, just return the result to handle it
569
- // as normal
570
- return result;
571
- }
572
- }
573
- }
574
-
575
- return notFound();
576
- }
577
-
578
- async function loadClientAndServerData(
579
- url: URL,
580
- params: Record<string, any>,
581
- serverParams: ServerLoadEvent,
582
- data: Record<string, any>,
583
- clientEndPoint?: PageEndPoint,
584
- serverEndPoint?: PageServerEndPoint,
585
- ) {
586
- if (clientEndPoint?.load) {
587
- const clientParams = buildClientParams(url, params, data);
588
- const clientResponse = await clientEndPoint.load(clientParams);
589
- if (clientResponse) {
590
- if (clientResponse.ok) {
591
- if (clientResponse.headers.get("Content-Type")?.includes("application/json")) {
592
- Object.assign(data, await clientResponse.json());
593
- }
594
- } else {
595
- return clientResponse;
596
- }
597
- }
598
- }
599
- if (serverEndPoint?.load) {
600
- const serverResponse = await serverEndPoint.load(serverParams);
601
- if (serverResponse) {
602
- if (serverResponse.ok) {
603
- if (serverResponse.headers.get("Content-Type")?.includes("application/json")) {
604
- Object.assign(data, await serverResponse.json());
605
- }
606
- } else {
607
- return serverResponse;
608
- }
609
- }
610
- }
611
- }
612
-
613
- function buildClientParams(url: URL, params: Record<string, any>, data: Record<string, any>) {
614
- return {
615
- url,
616
- params,
617
- data,
618
- };
619
- }
620
-
621
- function buildServerParams(
622
- ev: ServerEvent,
623
- url: URL,
624
- params: Record<string, any>,
625
- values: { json?: unknown; form?: unknown; query?: unknown } = {},
626
- ): ServerLoadEvent {
627
- return {
628
- url,
629
- params,
630
- appData: {},
631
- request: ev.request,
632
- // The inputs may have been validated already, in which case the request
633
- // body has been consumed and the parsed values are returned instead.
634
- // Their types come from the endpoint's schemas, so they are loosely
635
- // typed here
636
- json: (): Promise<any> =>
637
- values.json !== undefined ? Promise.resolve(values.json) : ev.json(),
638
- form: (): Promise<any> =>
639
- values.form !== undefined ? Promise.resolve(values.form) : formDataToRecord(ev.request),
640
- query: (): Promise<any> =>
641
- values.query !== undefined
642
- ? Promise.resolve(values.query)
643
- : Promise.resolve(searchParamsToRecord(url.searchParams)),
644
- cookies: ev.cookies,
645
- headers: ev.headers,
646
- adapter: ev.adapter,
647
- };
648
- }
@@ -1,25 +0,0 @@
1
- import { $watch } from "@torpor/view";
2
- import type PageState from "../types/PageState";
3
-
4
- const PAGE_SYMBOL: unique symbol = Symbol.for("torp.Page");
5
-
6
- const $page: PageState =
7
- // @ts-ignore
8
- (globalThis[PAGE_SYMBOL] ??=
9
- // The server request path uses `state/$serverPage`, which stores the
10
- // same state (same `globalThis` slot) without a reactive proxy
11
- $watch(
12
- {
13
- status: 404,
14
- url: new URL("http://localhost"),
15
- form: undefined,
16
- error: {
17
- message: "",
18
- },
19
- },
20
- {
21
- shallow: true,
22
- },
23
- ));
24
-
25
- export default $page;
@@ -1,22 +0,0 @@
1
- import { $watch } from "@torpor/view/ssr";
2
- import type PageState from "../types/PageState";
3
-
4
- const PAGE_SYMBOL: unique symbol = Symbol.for("torp.Page");
5
-
6
- // The server request path has no reactive subscribers, so the state is
7
- // stored plainly ($watch from `@torpor/view/ssr` is a no-op that returns
8
- // the object itself) instead of through a reactive proxy. Shares the same
9
- // `globalThis` slot as the client `$page`, so both variants always see the
10
- // same state object whichever loads first.
11
- const $page: PageState =
12
- // @ts-ignore
13
- (globalThis[PAGE_SYMBOL] ??= $watch({
14
- status: 404,
15
- url: new URL("http://localhost"),
16
- form: undefined,
17
- error: {
18
- message: "",
19
- },
20
- }));
21
-
22
- export default $page;