@torpor/build 0.4.13 → 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.
- package/README.md +20 -2
- package/dist/Jsonify-Dg6gChd3.d.mts +10 -0
- package/dist/Jsonify-Dg6gChd3.d.mts.map +1 -0
- package/dist/ParseRouteParams-DBmqlmLh.d.mts +42 -0
- package/dist/ParseRouteParams-DBmqlmLh.d.mts.map +1 -0
- package/dist/{RouteType-B6BMOyYw.mjs → Router-C4ADUfhv.mjs} +30 -42
- package/dist/Router-C4ADUfhv.mjs.map +1 -0
- package/dist/{Server-CF5wDJp6.d.mts → Server-C_IKav_e.d.mts} +44 -16
- package/dist/Server-C_IKav_e.d.mts.map +1 -0
- package/dist/{ServerEvent-CxPwA4lH.mjs → ServerEvent-Dljn5i7A.mjs} +37 -11
- package/dist/ServerEvent-Dljn5i7A.mjs.map +1 -0
- package/dist/Site-CMa153FA.d.mts +565 -0
- package/dist/Site-CMa153FA.d.mts.map +1 -0
- package/dist/StandardSchema-D28L8_zZ.d.mts +100 -0
- package/dist/StandardSchema-D28L8_zZ.d.mts.map +1 -0
- package/dist/TypedResponse-6lznqN4U.d.mts +22 -0
- package/dist/TypedResponse-6lznqN4U.d.mts.map +1 -0
- package/dist/{_page-BQs4WvCO.mjs → _page-4cQKs_Dn.mjs} +3 -5
- package/dist/_page-4cQKs_Dn.mjs.map +1 -0
- package/dist/bin/index.d.mts +1 -1
- package/dist/bin/index.js +15 -11
- package/dist/bin/index.js.map +1 -1
- package/dist/flattenHeaders-C_YYLdOq.mjs +114 -0
- package/dist/flattenHeaders-C_YYLdOq.mjs.map +1 -0
- package/dist/form.d.mts +35 -0
- package/dist/form.d.mts.map +1 -0
- package/dist/form.mjs +48 -0
- package/dist/form.mjs.map +1 -0
- package/dist/index.d.mts +72 -156
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +164 -32
- package/dist/index.mjs.map +1 -1
- package/dist/nav.d.mts +58 -5
- package/dist/nav.d.mts.map +1 -1
- package/dist/nav.mjs +116 -38
- package/dist/nav.mjs.map +1 -1
- package/dist/openapi.d.mts +144 -0
- package/dist/openapi.d.mts.map +1 -0
- package/dist/openapi.mjs +2 -0
- package/dist/pathTrie-D4Ax2Mu9.mjs +160 -0
- package/dist/pathTrie-D4Ax2Mu9.mjs.map +1 -0
- package/dist/plugin-D8Www11B.mjs +202 -0
- package/dist/plugin-D8Www11B.mjs.map +1 -0
- package/dist/{response-C5TtAsh1.mjs → response-Bg4w2S1q.mjs} +16 -39
- package/dist/response-Bg4w2S1q.mjs.map +1 -0
- package/dist/response.d.mts +309 -289
- package/dist/response.d.mts.map +1 -1
- package/dist/response.mjs +3 -4
- package/dist/run.d.mts +11 -4
- package/dist/run.d.mts.map +1 -1
- package/dist/run.mjs +2 -8
- package/dist/runOpenApi-BUi1RH1l.mjs +1381 -0
- package/dist/runOpenApi-BUi1RH1l.mjs.map +1 -0
- package/dist/schema.d.mts +32 -0
- package/dist/schema.d.mts.map +1 -0
- package/dist/schema.mjs +2 -0
- package/dist/server.d.mts +10 -8
- package/dist/server.d.mts.map +1 -1
- package/dist/server.mjs +132 -4
- package/dist/server.mjs.map +1 -0
- package/dist/state.d.mts.map +1 -1
- package/dist/state.mjs +2 -3
- package/dist/test.d.mts +8 -10
- package/dist/test.d.mts.map +1 -1
- package/dist/test.mjs +286 -82
- package/dist/test.mjs.map +1 -1
- package/dist/{seeOther-B4Yhu9iq.mjs → unprocessable-DAZbrDeR.mjs} +34 -13
- package/dist/unprocessable-DAZbrDeR.mjs.map +1 -0
- package/dist/validate-Ok4krlkX.mjs +40 -0
- package/dist/validate-Ok4krlkX.mjs.map +1 -0
- package/package.json +28 -17
- package/src/bin/index.ts +13 -2
- package/src/dev.ts +10 -0
- package/src/form/formDataToRecord.ts +23 -0
- package/src/form/readForm.ts +71 -0
- package/src/form.ts +4 -0
- package/src/index.ts +53 -1
- package/src/nav/api.test-d.ts +79 -0
- package/src/nav/api.ts +104 -0
- package/src/nav/formSubmit.ts +18 -4
- package/src/nav/navigate.ts +40 -15
- package/src/nav/route.ts +38 -0
- package/src/nav.ts +3 -1
- package/src/openapi/docsHtml.ts +25 -0
- package/src/openapi/document.ts +154 -0
- package/src/openapi/plugin.ts +78 -0
- package/src/openapi/types.ts +82 -0
- package/src/openapi.ts +14 -0
- package/src/response/TypedResponse.ts +17 -0
- package/src/response/badRequest.ts +14 -2
- package/src/response/created.ts +13 -2
- package/src/response/found.ts +2 -2
- package/src/response/movedPermanently.ts +2 -2
- package/src/response/notModified.ts +2 -2
- package/src/response/ok.ts +13 -4
- package/src/response/response.ts +4 -4
- package/src/response/unprocessable.ts +14 -2
- package/src/run/depCache.ts +175 -0
- package/src/run/devPlugin.ts +128 -0
- package/src/run/prepareTemplate.ts +6 -3
- package/src/run/run.ts +63 -39
- package/src/run/runBuild.ts +63 -12
- package/src/run/runDev.ts +146 -58
- package/src/run/runOpenApi.ts +52 -0
- package/src/run/runPreview.ts +17 -26
- package/src/run/staleTorpCopies.ts +100 -0
- package/src/run.ts +2 -1
- package/src/schema.ts +7 -0
- package/src/server/CookieHelper.ts +17 -7
- package/src/server/Server.ts +54 -30
- package/src/server/ServerEvent.ts +24 -1
- package/src/server/connect/connectMiddleware.ts +44 -40
- package/src/server/connect/flattenHeaders.ts +1 -23
- package/src/server/connect/requestToNodeMessage.ts +6 -3
- package/src/server/types/MiddlewareFunction.ts +18 -4
- package/src/site/Router.ts +52 -41
- package/src/site/Site.ts +184 -10
- package/src/site/checkLayoutSlots.ts +114 -0
- package/src/site/checkRoutes.ts +435 -0
- package/src/site/clientEntry.ts +22 -9
- package/src/site/layoutSlots.ts +53 -0
- package/src/site/manifest.ts +125 -6
- package/src/site/serverEntry.ts +352 -133
- package/src/state/$page.ts +2 -1
- package/src/state/$serverPage.ts +22 -0
- package/src/test/runTest.ts +306 -118
- package/src/types/Adapter.ts +12 -0
- package/src/types/Jsonify.ts +20 -0
- package/src/types/PageData.test-d.ts +136 -0
- package/src/types/PageData.ts +35 -0
- package/src/types/PageEndPoint.ts +25 -9
- package/src/types/PageForm.test-d.ts +78 -0
- package/src/types/PageForm.ts +29 -0
- package/src/types/PageLoadEvent.ts +16 -4
- package/src/types/PageLoadReturn.ts +13 -0
- package/src/types/PageProps.ts +14 -0
- package/src/types/PageServerAction.ts +8 -2
- package/src/types/PageServerEndPoint.test-d.ts +105 -0
- package/src/types/PageServerEndPoint.ts +99 -5
- package/src/types/PageServerLoad.ts +11 -3
- package/src/types/ParseRouteParams.test-d.ts +84 -0
- package/src/types/ParseRouteParams.ts +64 -0
- package/src/types/Route.ts +20 -1
- package/src/types/RouteHandler.ts +8 -1
- package/src/types/ServerEndPoint.test-d.ts +77 -0
- package/src/types/ServerEndPoint.ts +148 -16
- package/src/types/ServerHook.ts +10 -3
- package/src/types/ServerLoadEvent.ts +48 -3
- package/src/types/ServerRequest.ts +10 -2
- package/src/types/SitePlugin.ts +24 -0
- package/src/types/StandardSchema.ts +109 -0
- package/src/utils/pathToRegex.ts +4 -2
- package/src/utils/pathTrie.ts +182 -0
- package/src/utils/searchParamsToRecord.ts +18 -0
- package/src/utils/torporPackages.ts +150 -0
- package/src/utils/tsconfigAliases.ts +90 -0
- package/src/validation/ValidationError.ts +18 -0
- package/src/validation/endpoint.ts +83 -0
- package/src/validation/validate.ts +26 -0
- package/dist/RouteType-B6BMOyYw.mjs.map +0 -1
- package/dist/Server-CF5wDJp6.d.mts.map +0 -1
- package/dist/ServerEvent-CxPwA4lH.mjs.map +0 -1
- package/dist/Site-DgC6WWn1.d.mts +0 -78
- package/dist/Site-DgC6WWn1.d.mts.map +0 -1
- package/dist/_page-BQs4WvCO.mjs.map +0 -1
- package/dist/connectMiddleware-D7nehjj3.mjs +0 -252
- package/dist/connectMiddleware-D7nehjj3.mjs.map +0 -1
- package/dist/pathToRegex-TUAMc3zS.mjs +0 -11
- package/dist/pathToRegex-TUAMc3zS.mjs.map +0 -1
- package/dist/response-C5TtAsh1.mjs.map +0 -1
- package/dist/run-DiZgkMnG.mjs +0 -332
- package/dist/run-DiZgkMnG.mjs.map +0 -1
- package/dist/seeOther-B4Yhu9iq.mjs.map +0 -1
- package/src/server/Routerx.ts +0 -72
- package/src/server/connect/bufferToArrayBuffer.ts +0 -8
- package/src/server/connect/readableToBuffer.ts +0 -16
package/src/site/serverEntry.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import manifest from "@torpor/build/manifest";
|
|
2
2
|
import { type ServerComponent, type ServerSlotRender } from "@torpor/view/ssr";
|
|
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
|
-
import $page from "../state/$
|
|
8
|
+
import $page from "../state/$serverPage.ts";
|
|
8
9
|
import type PageEndPoint from "../types/PageEndPoint.ts";
|
|
9
10
|
import type PageServerEndPoint from "../types/PageServerEndPoint.ts";
|
|
10
11
|
import type RouteHandler from "../types/RouteHandler.ts";
|
|
12
|
+
import type ServerRequest from "../types/ServerRequest.ts";
|
|
11
13
|
import {
|
|
12
14
|
ERROR_ROUTE,
|
|
13
15
|
HOOK_ROUTE,
|
|
@@ -21,6 +23,15 @@ import {
|
|
|
21
23
|
import type ServerEndPoint from "../types/ServerEndPoint.ts";
|
|
22
24
|
import type ServerHook from "../types/ServerHook.ts";
|
|
23
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";
|
|
24
35
|
import Router from "./Router.ts";
|
|
25
36
|
|
|
26
37
|
// Build the router from the Site object created by the user
|
|
@@ -29,14 +40,24 @@ router.addPages(manifest.routes);
|
|
|
29
40
|
|
|
30
41
|
//console.log(`routes:\n ${router.routes.map((r) => r.path).join("\n ")}`);
|
|
31
42
|
|
|
32
|
-
export async function load(ev: ServerEvent, template
|
|
33
|
-
const url =
|
|
43
|
+
export async function load(ev: ServerEvent, template?: string): Promise<Response> {
|
|
44
|
+
const url = ev.url;
|
|
34
45
|
const path = url.pathname;
|
|
35
46
|
const query = url.searchParams;
|
|
36
47
|
|
|
37
48
|
//console.log(`handling ${ev.request.method} for '${path}'${query.size ? ` with ${query}` : ""}`);
|
|
38
49
|
|
|
39
|
-
|
|
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
|
+
}
|
|
40
61
|
if (!route) {
|
|
41
62
|
return handleResponse(notFound());
|
|
42
63
|
}
|
|
@@ -78,8 +99,8 @@ export async function load(ev: ServerEvent, template: string): Promise<Response>
|
|
|
78
99
|
if (ev.request.method === "GET") {
|
|
79
100
|
return await loadData(ev, url, handler, "load", params);
|
|
80
101
|
} else if (ev.request.method === "POST") {
|
|
81
|
-
const serverEndPoint: PageServerEndPoint | undefined =
|
|
82
|
-
|
|
102
|
+
const serverEndPoint: PageServerEndPoint | undefined = (await resolveModule(handler))
|
|
103
|
+
.default;
|
|
83
104
|
return handleResponse(
|
|
84
105
|
await runAction(ev, url, handler, serverEndPoint, params, query, template),
|
|
85
106
|
true,
|
|
@@ -104,6 +125,22 @@ export async function load(ev: ServerEvent, template: string): Promise<Response>
|
|
|
104
125
|
return notFound();
|
|
105
126
|
}
|
|
106
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
|
+
|
|
107
144
|
async function handleResponse(response: Response, fromForm = false): Promise<Response> {
|
|
108
145
|
$page.status = response.status;
|
|
109
146
|
|
|
@@ -145,151 +182,272 @@ async function loadData(
|
|
|
145
182
|
url: URL,
|
|
146
183
|
handler: RouteHandler,
|
|
147
184
|
functionName: string,
|
|
148
|
-
params: Record<string,
|
|
185
|
+
params: Record<string, any>,
|
|
149
186
|
) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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;
|
|
159
210
|
}
|
|
211
|
+
}
|
|
160
212
|
|
|
161
|
-
|
|
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
|
+
}
|
|
162
242
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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;
|
|
166
257
|
}
|
|
167
|
-
|
|
168
|
-
return notFound();
|
|
169
258
|
}
|
|
170
259
|
|
|
171
260
|
async function loadView(
|
|
172
261
|
ev: ServerEvent,
|
|
173
262
|
url: URL,
|
|
174
263
|
handler: RouteHandler,
|
|
175
|
-
params: Record<string,
|
|
176
|
-
template: string,
|
|
264
|
+
params: Record<string, any>,
|
|
265
|
+
template: string | undefined,
|
|
177
266
|
formStatus?: number,
|
|
178
267
|
form?: Record<string, string | number>,
|
|
268
|
+
skipHook?: boolean,
|
|
179
269
|
) {
|
|
180
270
|
// There must be a client endpoint with a component
|
|
181
|
-
const clientEndPoint: PageEndPoint | undefined = (await handler
|
|
271
|
+
const clientEndPoint: PageEndPoint | undefined = (await resolveModule(handler)).default;
|
|
182
272
|
if (!clientEndPoint?.component) {
|
|
183
273
|
return notFound();
|
|
184
274
|
}
|
|
185
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
|
+
|
|
186
283
|
// There may be a server endpoint
|
|
187
284
|
const serverEndPoint: PageServerEndPoint | undefined =
|
|
188
285
|
handler.serverEndPoint && (await handler.serverEndPoint())?.default;
|
|
189
286
|
|
|
190
|
-
//
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
+
}
|
|
196
308
|
}
|
|
197
309
|
}
|
|
198
310
|
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
let
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
serverParams,
|
|
213
|
-
data,
|
|
214
|
-
layoutEndPoint,
|
|
215
|
-
layoutServerEndPoint,
|
|
216
|
-
);
|
|
217
|
-
if (layoutResponse?.ok === false) {
|
|
218
|
-
return layoutResponse;
|
|
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;
|
|
219
324
|
}
|
|
220
325
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
+
};
|
|
249
383
|
};
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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;
|
|
261
406
|
});
|
|
262
|
-
|
|
263
|
-
return body;
|
|
264
|
-
};
|
|
407
|
+
}
|
|
265
408
|
}
|
|
266
409
|
}
|
|
267
410
|
}
|
|
268
|
-
}
|
|
269
411
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
+
}
|
|
273
420
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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);
|
|
277
427
|
}
|
|
278
428
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
+
}
|
|
286
437
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
+
}
|
|
293
451
|
}
|
|
294
452
|
|
|
295
453
|
async function runAction(
|
|
@@ -297,26 +455,72 @@ async function runAction(
|
|
|
297
455
|
url: URL,
|
|
298
456
|
handler: RouteHandler,
|
|
299
457
|
serverEndPoint: PageServerEndPoint | undefined,
|
|
300
|
-
params: Record<string,
|
|
458
|
+
params: Record<string, any>,
|
|
301
459
|
query: URLSearchParams,
|
|
302
|
-
template: string,
|
|
460
|
+
template: string | undefined,
|
|
303
461
|
) {
|
|
304
462
|
const actionName = (Array.from(query.keys())[0] || "default").replace(/^\//, "");
|
|
305
463
|
if (serverEndPoint?.actions) {
|
|
306
464
|
const action = serverEndPoint.actions[actionName];
|
|
307
465
|
if (action) {
|
|
308
|
-
|
|
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
|
+
}
|
|
309
484
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
+
}
|
|
314
516
|
}
|
|
517
|
+
} catch (error) {
|
|
518
|
+
const response = validationErrorResponse(error);
|
|
519
|
+
if (response) return response;
|
|
520
|
+
throw error;
|
|
315
521
|
}
|
|
316
522
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
if (ev.request.headers.has("x-torpor-form-submit")) {
|
|
523
|
+
if (ev.request.headers.has("X-Torpor-Form-Submit")) {
|
|
320
524
|
// If the form was submitted from javascript, just return the
|
|
321
525
|
// response and it will be handled on the client
|
|
322
526
|
// TODO: Should we make the user return a response??
|
|
@@ -324,10 +528,10 @@ async function runAction(
|
|
|
324
528
|
|
|
325
529
|
// If it's a redirect, we need to stop the browser handling it
|
|
326
530
|
if (result.status >= 300 && result.status <= 399) {
|
|
327
|
-
const location = result.headers.get("
|
|
531
|
+
const location = result.headers.get("Location") ?? "";
|
|
328
532
|
result = ok();
|
|
329
|
-
result.headers.set("
|
|
330
|
-
result.headers.set("
|
|
533
|
+
result.headers.set("X-Torpor-Form-Redirect", "");
|
|
534
|
+
result.headers.set("Location", location);
|
|
331
535
|
}
|
|
332
536
|
|
|
333
537
|
return result;
|
|
@@ -338,9 +542,11 @@ async function runAction(
|
|
|
338
542
|
) {
|
|
339
543
|
// If the form was submitted without javascript, and there was
|
|
340
544
|
// no result, an ok result, or a 4xx error, re-render the view
|
|
341
|
-
// with the form result
|
|
342
|
-
//
|
|
343
|
-
|
|
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);
|
|
344
550
|
const formStatus = result?.status;
|
|
345
551
|
let formMessage = await result?.text();
|
|
346
552
|
if (formMessage && result?.headers.get("Content-Type")?.includes("application/json")) {
|
|
@@ -356,7 +562,7 @@ async function runAction(
|
|
|
356
562
|
// object with whatever info you need
|
|
357
563
|
$page.form!.status ??= formStatus ?? 0;
|
|
358
564
|
$page.form!.message ??= formMessage ?? "";
|
|
359
|
-
return await loadView(ev, newUrl, handler, params, template, formStatus, $page.form);
|
|
565
|
+
return await loadView(ev, newUrl, handler, params, template, formStatus, $page.form, true);
|
|
360
566
|
} else {
|
|
361
567
|
// If the form was submitted without javascript and there was a
|
|
362
568
|
// redirect or server error, just return the result to handle it
|
|
@@ -371,7 +577,7 @@ async function runAction(
|
|
|
371
577
|
|
|
372
578
|
async function loadClientAndServerData(
|
|
373
579
|
url: URL,
|
|
374
|
-
params: Record<string,
|
|
580
|
+
params: Record<string, any>,
|
|
375
581
|
serverParams: ServerLoadEvent,
|
|
376
582
|
data: Record<string, any>,
|
|
377
583
|
clientEndPoint?: PageEndPoint,
|
|
@@ -404,7 +610,7 @@ async function loadClientAndServerData(
|
|
|
404
610
|
}
|
|
405
611
|
}
|
|
406
612
|
|
|
407
|
-
function buildClientParams(url: URL, params: Record<string,
|
|
613
|
+
function buildClientParams(url: URL, params: Record<string, any>, data: Record<string, any>) {
|
|
408
614
|
return {
|
|
409
615
|
url,
|
|
410
616
|
params,
|
|
@@ -415,13 +621,26 @@ function buildClientParams(url: URL, params: Record<string, string>, data: Recor
|
|
|
415
621
|
function buildServerParams(
|
|
416
622
|
ev: ServerEvent,
|
|
417
623
|
url: URL,
|
|
418
|
-
params: Record<string,
|
|
624
|
+
params: Record<string, any>,
|
|
625
|
+
values: { json?: unknown; form?: unknown; query?: unknown } = {},
|
|
419
626
|
): ServerLoadEvent {
|
|
420
627
|
return {
|
|
421
628
|
url,
|
|
422
629
|
params,
|
|
423
630
|
appData: {},
|
|
424
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)),
|
|
425
644
|
cookies: ev.cookies,
|
|
426
645
|
headers: ev.headers,
|
|
427
646
|
adapter: ev.adapter,
|
package/src/state/$page.ts
CHANGED
|
@@ -6,7 +6,8 @@ const PAGE_SYMBOL: unique symbol = Symbol.for("torp.Page");
|
|
|
6
6
|
const $page: PageState =
|
|
7
7
|
// @ts-ignore
|
|
8
8
|
(globalThis[PAGE_SYMBOL] ??=
|
|
9
|
-
//
|
|
9
|
+
// The server request path uses `state/$serverPage`, which stores the
|
|
10
|
+
// same state (same `globalThis` slot) without a reactive proxy
|
|
10
11
|
$watch(
|
|
11
12
|
{
|
|
12
13
|
status: 404,
|
|
@@ -0,0 +1,22 @@
|
|
|
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;
|