@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.
- 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 +158 -26
- 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 +283 -79
- 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 +341 -132
- 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-BPFtSYA3.mjs +0 -332
- package/dist/run-BPFtSYA3.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/test/runTest.ts
CHANGED
|
@@ -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/$
|
|
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: () =>
|
|
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 =
|
|
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
|
-
|
|
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,
|
|
160
|
+
params: Record<string, any>,
|
|
122
161
|
) {
|
|
123
162
|
const serverEndPoint: ServerEndPoint | undefined = (await handler.endPoint()).default;
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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,
|
|
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
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
//
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
let
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
package/src/types/Adapter.ts
CHANGED
|
@@ -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;
|