@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.
Files changed (176) hide show
  1. package/README.md +20 -2
  2. package/dist/Jsonify-Dg6gChd3.d.mts +10 -0
  3. package/dist/Jsonify-Dg6gChd3.d.mts.map +1 -0
  4. package/dist/ParseRouteParams-DBmqlmLh.d.mts +42 -0
  5. package/dist/ParseRouteParams-DBmqlmLh.d.mts.map +1 -0
  6. package/dist/{RouteType-B6BMOyYw.mjs → Router-C4ADUfhv.mjs} +30 -42
  7. package/dist/Router-C4ADUfhv.mjs.map +1 -0
  8. package/dist/{Server-CF5wDJp6.d.mts → Server-C_IKav_e.d.mts} +44 -16
  9. package/dist/Server-C_IKav_e.d.mts.map +1 -0
  10. package/dist/{ServerEvent-CxPwA4lH.mjs → ServerEvent-Dljn5i7A.mjs} +37 -11
  11. package/dist/ServerEvent-Dljn5i7A.mjs.map +1 -0
  12. package/dist/Site-CMa153FA.d.mts +565 -0
  13. package/dist/Site-CMa153FA.d.mts.map +1 -0
  14. package/dist/StandardSchema-D28L8_zZ.d.mts +100 -0
  15. package/dist/StandardSchema-D28L8_zZ.d.mts.map +1 -0
  16. package/dist/TypedResponse-6lznqN4U.d.mts +22 -0
  17. package/dist/TypedResponse-6lznqN4U.d.mts.map +1 -0
  18. package/dist/{_page-BQs4WvCO.mjs → _page-4cQKs_Dn.mjs} +3 -5
  19. package/dist/_page-4cQKs_Dn.mjs.map +1 -0
  20. package/dist/bin/index.d.mts +1 -1
  21. package/dist/bin/index.js +15 -11
  22. package/dist/bin/index.js.map +1 -1
  23. package/dist/flattenHeaders-C_YYLdOq.mjs +114 -0
  24. package/dist/flattenHeaders-C_YYLdOq.mjs.map +1 -0
  25. package/dist/form.d.mts +35 -0
  26. package/dist/form.d.mts.map +1 -0
  27. package/dist/form.mjs +48 -0
  28. package/dist/form.mjs.map +1 -0
  29. package/dist/index.d.mts +72 -156
  30. package/dist/index.d.mts.map +1 -1
  31. package/dist/index.mjs +164 -32
  32. package/dist/index.mjs.map +1 -1
  33. package/dist/nav.d.mts +58 -5
  34. package/dist/nav.d.mts.map +1 -1
  35. package/dist/nav.mjs +116 -38
  36. package/dist/nav.mjs.map +1 -1
  37. package/dist/openapi.d.mts +144 -0
  38. package/dist/openapi.d.mts.map +1 -0
  39. package/dist/openapi.mjs +2 -0
  40. package/dist/pathTrie-D4Ax2Mu9.mjs +160 -0
  41. package/dist/pathTrie-D4Ax2Mu9.mjs.map +1 -0
  42. package/dist/plugin-D8Www11B.mjs +202 -0
  43. package/dist/plugin-D8Www11B.mjs.map +1 -0
  44. package/dist/{response-C5TtAsh1.mjs → response-Bg4w2S1q.mjs} +16 -39
  45. package/dist/response-Bg4w2S1q.mjs.map +1 -0
  46. package/dist/response.d.mts +309 -289
  47. package/dist/response.d.mts.map +1 -1
  48. package/dist/response.mjs +3 -4
  49. package/dist/run.d.mts +11 -4
  50. package/dist/run.d.mts.map +1 -1
  51. package/dist/run.mjs +2 -8
  52. package/dist/runOpenApi-BUi1RH1l.mjs +1381 -0
  53. package/dist/runOpenApi-BUi1RH1l.mjs.map +1 -0
  54. package/dist/schema.d.mts +32 -0
  55. package/dist/schema.d.mts.map +1 -0
  56. package/dist/schema.mjs +2 -0
  57. package/dist/server.d.mts +10 -8
  58. package/dist/server.d.mts.map +1 -1
  59. package/dist/server.mjs +132 -4
  60. package/dist/server.mjs.map +1 -0
  61. package/dist/state.d.mts.map +1 -1
  62. package/dist/state.mjs +2 -3
  63. package/dist/test.d.mts +8 -10
  64. package/dist/test.d.mts.map +1 -1
  65. package/dist/test.mjs +286 -82
  66. package/dist/test.mjs.map +1 -1
  67. package/dist/{seeOther-B4Yhu9iq.mjs → unprocessable-DAZbrDeR.mjs} +34 -13
  68. package/dist/unprocessable-DAZbrDeR.mjs.map +1 -0
  69. package/dist/validate-Ok4krlkX.mjs +40 -0
  70. package/dist/validate-Ok4krlkX.mjs.map +1 -0
  71. package/package.json +28 -17
  72. package/src/bin/index.ts +13 -2
  73. package/src/dev.ts +10 -0
  74. package/src/form/formDataToRecord.ts +23 -0
  75. package/src/form/readForm.ts +71 -0
  76. package/src/form.ts +4 -0
  77. package/src/index.ts +53 -1
  78. package/src/nav/api.test-d.ts +79 -0
  79. package/src/nav/api.ts +104 -0
  80. package/src/nav/formSubmit.ts +18 -4
  81. package/src/nav/navigate.ts +40 -15
  82. package/src/nav/route.ts +38 -0
  83. package/src/nav.ts +3 -1
  84. package/src/openapi/docsHtml.ts +25 -0
  85. package/src/openapi/document.ts +154 -0
  86. package/src/openapi/plugin.ts +78 -0
  87. package/src/openapi/types.ts +82 -0
  88. package/src/openapi.ts +14 -0
  89. package/src/response/TypedResponse.ts +17 -0
  90. package/src/response/badRequest.ts +14 -2
  91. package/src/response/created.ts +13 -2
  92. package/src/response/found.ts +2 -2
  93. package/src/response/movedPermanently.ts +2 -2
  94. package/src/response/notModified.ts +2 -2
  95. package/src/response/ok.ts +13 -4
  96. package/src/response/response.ts +4 -4
  97. package/src/response/unprocessable.ts +14 -2
  98. package/src/run/depCache.ts +175 -0
  99. package/src/run/devPlugin.ts +128 -0
  100. package/src/run/prepareTemplate.ts +6 -3
  101. package/src/run/run.ts +63 -39
  102. package/src/run/runBuild.ts +63 -12
  103. package/src/run/runDev.ts +146 -58
  104. package/src/run/runOpenApi.ts +52 -0
  105. package/src/run/runPreview.ts +17 -26
  106. package/src/run/staleTorpCopies.ts +100 -0
  107. package/src/run.ts +2 -1
  108. package/src/schema.ts +7 -0
  109. package/src/server/CookieHelper.ts +17 -7
  110. package/src/server/Server.ts +54 -30
  111. package/src/server/ServerEvent.ts +24 -1
  112. package/src/server/connect/connectMiddleware.ts +44 -40
  113. package/src/server/connect/flattenHeaders.ts +1 -23
  114. package/src/server/connect/requestToNodeMessage.ts +6 -3
  115. package/src/server/types/MiddlewareFunction.ts +18 -4
  116. package/src/site/Router.ts +52 -41
  117. package/src/site/Site.ts +184 -10
  118. package/src/site/checkLayoutSlots.ts +114 -0
  119. package/src/site/checkRoutes.ts +435 -0
  120. package/src/site/clientEntry.ts +22 -9
  121. package/src/site/layoutSlots.ts +53 -0
  122. package/src/site/manifest.ts +125 -6
  123. package/src/site/serverEntry.ts +352 -133
  124. package/src/state/$page.ts +2 -1
  125. package/src/state/$serverPage.ts +22 -0
  126. package/src/test/runTest.ts +306 -118
  127. package/src/types/Adapter.ts +12 -0
  128. package/src/types/Jsonify.ts +20 -0
  129. package/src/types/PageData.test-d.ts +136 -0
  130. package/src/types/PageData.ts +35 -0
  131. package/src/types/PageEndPoint.ts +25 -9
  132. package/src/types/PageForm.test-d.ts +78 -0
  133. package/src/types/PageForm.ts +29 -0
  134. package/src/types/PageLoadEvent.ts +16 -4
  135. package/src/types/PageLoadReturn.ts +13 -0
  136. package/src/types/PageProps.ts +14 -0
  137. package/src/types/PageServerAction.ts +8 -2
  138. package/src/types/PageServerEndPoint.test-d.ts +105 -0
  139. package/src/types/PageServerEndPoint.ts +99 -5
  140. package/src/types/PageServerLoad.ts +11 -3
  141. package/src/types/ParseRouteParams.test-d.ts +84 -0
  142. package/src/types/ParseRouteParams.ts +64 -0
  143. package/src/types/Route.ts +20 -1
  144. package/src/types/RouteHandler.ts +8 -1
  145. package/src/types/ServerEndPoint.test-d.ts +77 -0
  146. package/src/types/ServerEndPoint.ts +148 -16
  147. package/src/types/ServerHook.ts +10 -3
  148. package/src/types/ServerLoadEvent.ts +48 -3
  149. package/src/types/ServerRequest.ts +10 -2
  150. package/src/types/SitePlugin.ts +24 -0
  151. package/src/types/StandardSchema.ts +109 -0
  152. package/src/utils/pathToRegex.ts +4 -2
  153. package/src/utils/pathTrie.ts +182 -0
  154. package/src/utils/searchParamsToRecord.ts +18 -0
  155. package/src/utils/torporPackages.ts +150 -0
  156. package/src/utils/tsconfigAliases.ts +90 -0
  157. package/src/validation/ValidationError.ts +18 -0
  158. package/src/validation/endpoint.ts +83 -0
  159. package/src/validation/validate.ts +26 -0
  160. package/dist/RouteType-B6BMOyYw.mjs.map +0 -1
  161. package/dist/Server-CF5wDJp6.d.mts.map +0 -1
  162. package/dist/ServerEvent-CxPwA4lH.mjs.map +0 -1
  163. package/dist/Site-DgC6WWn1.d.mts +0 -78
  164. package/dist/Site-DgC6WWn1.d.mts.map +0 -1
  165. package/dist/_page-BQs4WvCO.mjs.map +0 -1
  166. package/dist/connectMiddleware-D7nehjj3.mjs +0 -252
  167. package/dist/connectMiddleware-D7nehjj3.mjs.map +0 -1
  168. package/dist/pathToRegex-TUAMc3zS.mjs +0 -11
  169. package/dist/pathToRegex-TUAMc3zS.mjs.map +0 -1
  170. package/dist/response-C5TtAsh1.mjs.map +0 -1
  171. package/dist/run-DiZgkMnG.mjs +0 -332
  172. package/dist/run-DiZgkMnG.mjs.map +0 -1
  173. package/dist/seeOther-B4Yhu9iq.mjs.map +0 -1
  174. package/src/server/Routerx.ts +0 -72
  175. package/src/server/connect/bufferToArrayBuffer.ts +0 -8
  176. package/src/server/connect/readableToBuffer.ts +0 -16
@@ -0,0 +1,84 @@
1
+ import type PageEndPoint from "./PageEndPoint";
2
+ import type PageLoadEvent from "./PageLoadEvent";
3
+ import type PageServerEndPoint from "./PageServerEndPoint";
4
+ import type { ParseRouteParams, RouteArgs, RouteParamsOf } from "./ParseRouteParams";
5
+ import type ServerLoadEvent from "./ServerLoadEvent";
6
+ import route from "../nav/route";
7
+
8
+ type Equals<A, B> =
9
+ (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false;
10
+ type Mutual<A, B> = A extends B ? (B extends A ? true : false) : false;
11
+ type Expect<T extends true> = T;
12
+ type IsNever<T> = [T] extends [never] ? true : false;
13
+
14
+ // --- ParseRouteParams ---
15
+
16
+ export type T01 = Expect<Equals<ParseRouteParams<"/about">, unknown>>;
17
+ export type T02 = Expect<Equals<ParseRouteParams<"/">, unknown>>;
18
+ export type T03 = Expect<Equals<ParseRouteParams<"/posts/[id]">, { id: string }>>;
19
+ export type T04 = Expect<
20
+ Mutual<ParseRouteParams<"/users/[userId]/posts/[postId]">, { userId: string; postId: string }>
21
+ >;
22
+ export type T05 = Expect<Equals<ParseRouteParams<"/files/[...path]">, { path: string }>>;
23
+ export type T06 = Expect<Equals<ParseRouteParams<"/api/posts/[id]/~server">, { id: string }>>;
24
+ export type T07 = Expect<IsNever<keyof ParseRouteParams<"/about">>>;
25
+
26
+ export const postParams: ParseRouteParams<"/posts/[id]"> = { id: "5" };
27
+ export const paramValue: string = postParams.id;
28
+ // @ts-expect-error 'nope' is not a param of /posts/[id]
29
+ export const badParam: string = postParams.nope;
30
+ // @ts-expect-error 'id' is required
31
+ export const missingParam: ParseRouteParams<"/posts/[id]"> = {};
32
+
33
+ // --- RouteParamsOf (loose defaults) ---
34
+
35
+ export type T08 = Expect<Equals<RouteParamsOf<undefined>, Record<string, string>>>;
36
+ export type T09 = Expect<Equals<RouteParamsOf<string>, Record<string, string>>>;
37
+ export type T10 = Expect<Equals<RouteParamsOf<"/posts/[id]">, { id: string }>>;
38
+
39
+ // --- RouteArgs ---
40
+
41
+ export type T11 = Expect<Equals<RouteArgs<"/about">, []>>;
42
+ export type T12 = Expect<Equals<RouteArgs<"/posts/[id]">, [params: { id: string }]>>;
43
+
44
+ // --- Typed events ---
45
+
46
+ export const serverEvent: ServerLoadEvent<"/posts/[id]"> = null as never;
47
+ export const serverParam: string = serverEvent.params.id;
48
+ // @ts-expect-error 'nope' is not a param of /posts/[id]
49
+ export const badServerParam: string = serverEvent.params.nope;
50
+
51
+ export const pageEvent: PageLoadEvent<"/users/[userId]"> = null as never;
52
+ export const pageParam: string = pageEvent.params.userId;
53
+
54
+ // --- Usage sites ---
55
+
56
+ export const pageServer: PageServerEndPoint<"/posts/[id]"> = {
57
+ load: (ev) => void ev.params.id,
58
+ };
59
+
60
+ // @ts-expect-error 'nope' is not a param of /posts/[id]
61
+ export const badServer: PageServerEndPoint<"/posts/[id]"> = { load: (ev) => void ev.params.nope };
62
+
63
+ export const page: PageEndPoint<"/posts/[id]"> = {
64
+ load: (ev) => void ev.params.id,
65
+ };
66
+
67
+ // --- Typed link builder ---
68
+
69
+ export const staticRoute: string = route("/about");
70
+ export const dynamicRoute: string = route("/posts/[id]", { id: "5" });
71
+ export const splatRoute: string = route("/files/[...path]", { path: "a/b" });
72
+ // @ts-expect-error missing params
73
+ void route("/posts/[id]");
74
+ // @ts-expect-error params are not accepted for static routes
75
+ void route("/about", {});
76
+ // @ts-expect-error 'nope' is not a param of /posts/[id]
77
+ void route("/posts/[id]", { nope: "1" });
78
+ // @ts-expect-error 'extra' is not a param of /posts/[id]
79
+ void route("/posts/[id]", { id: "5", extra: "x" });
80
+ // @ts-expect-error 'id' is required
81
+ void route("/posts/[id]", {});
82
+
83
+ // Non-literal paths fall back to a loose optional params object
84
+ export const looseRoute: string = route("/posts/[id]" as string, { id: "5" });
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Extracts the params object from a route path string.
3
+ *
4
+ * Dynamic segments (`[id]`) and splat segments (`[...path]`) become string
5
+ * properties; static segments are ignored. For example:
6
+ *
7
+ * ```ts
8
+ * type Params = ParseRouteParams<"/posts/[id]/comments/[commentId]">;
9
+ * // { id: string; commentId: string }
10
+ * ```
11
+ */
12
+ export type ParseRouteParams<Route extends string> = ParseSegments<Route, unknown>;
13
+
14
+ type ParseSegments<Route extends string, Params> = Route extends `${infer Head}/${infer Rest}`
15
+ ? ParseSegments<Rest, ParseSegment<Head, Params>>
16
+ : ParseSegment<Route, Params>;
17
+
18
+ type ParseSegment<Segment extends string, Params> = Segment extends `[...${infer Name}]`
19
+ ? Params & { [Key in Name]: string }
20
+ : Segment extends `[${infer Name}]`
21
+ ? Params & { [Key in Name]: string }
22
+ : Params;
23
+
24
+ /**
25
+ * Resolves the params for a route annotation: the parsed params for a literal
26
+ * route path, or a loose record when no route (or a non-literal string) is
27
+ * given.
28
+ */
29
+ export type RouteParamsOf<Route extends string | undefined> = string extends Route
30
+ ? Record<string, string>
31
+ : Route extends string
32
+ ? ParseRouteParams<Route>
33
+ : Record<string, string>;
34
+
35
+ /**
36
+ * Validates an inferred params object against a route's params shape:
37
+ * resolves to the params type when its keys are exactly the route's keys, and
38
+ * `never` when keys are missing or extra — so wrong keys error at the call
39
+ * site (excess property checks are skipped for deferred conditional rest
40
+ * tuples, but assignability to `never` can't be).
41
+ */
42
+ export type ExactRouteParams<Params, Shape> = Params extends Shape
43
+ ? [Exclude<keyof Params, keyof Shape>] extends [never]
44
+ ? Params
45
+ : never
46
+ : never;
47
+
48
+ /**
49
+ * The arguments for building a route path: nothing for static routes, or the
50
+ * route's params object for dynamic routes.
51
+ */
52
+ export type RouteArgs<Route extends string> = string extends Route
53
+ ? [params?: Record<string, string>]
54
+ : keyof ParseRouteParams<Route> extends never
55
+ ? []
56
+ : [params: ParseRouteParams<Route>];
57
+
58
+ /**
59
+ * RouteArgs for an optional route annotation, defaulting to a single optional
60
+ * loose params object.
61
+ */
62
+ export type RouteArgsOf<Route extends string | undefined> = Route extends string
63
+ ? RouteArgs<Route>
64
+ : [params?: Record<string, string>];
@@ -1,11 +1,30 @@
1
+ import type PageEndPoint from "./PageEndPoint";
2
+ import type PageServerEndPoint from "./PageServerEndPoint";
3
+ import type ServerEndPoint from "./ServerEndPoint";
4
+ import type ServerHook from "./ServerHook";
1
5
  import { type RouteType } from "./RouteType";
2
6
 
7
+ /**
8
+ * Any endpoint shape that can be stored inline on a Route.
9
+ */
10
+ export type InlineEndPoint = PageServerEndPoint | ServerEndPoint | ServerHook | PageEndPoint;
11
+
3
12
  /**
4
13
  * A route that is added to the Site.
5
14
  */
6
15
  export default interface Route {
7
16
  path: string;
8
- file: string;
17
+ /**
18
+ * The file for this route, relative to the site root. Omitted for inline
19
+ * endpoints (see `endPoint`).
20
+ */
21
+ file?: string;
22
+ /**
23
+ * For inline routes (defined in code rather than by file), the endpoint
24
+ * object itself. The shape depends on the route type
25
+ * (`PageServerEndPoint`, `ServerEndPoint`, `ServerHook`, etc.).
26
+ */
27
+ endPoint?: InlineEndPoint;
9
28
  type: RouteType;
10
29
  subFolder?: string;
11
30
  }
@@ -1,4 +1,5 @@
1
1
  import type RouteLayoutHandler from "./RouteLayoutHandler";
2
+ import type ServerHook from "./ServerHook";
2
3
 
3
4
  /**
4
5
  * A handler for a route in the Router.
@@ -12,5 +13,11 @@ export default interface RouteHandler {
12
13
  loaded?: boolean;
13
14
  layouts?: RouteLayoutHandler[];
14
15
  serverEndPoint?: () => Promise<any>;
15
- serverHook?: () => Promise<any>;
16
+ serverHooks?: (() => Promise<any>)[];
17
+
18
+ // The endpoint module and the resolved server hooks are cached on the
19
+ // handler after first use, so warm requests don't pay for the loaders
20
+ modulePromise?: Promise<any>;
21
+ resolvedModule?: any;
22
+ resolvedHooks?: Promise<ServerHook[]>;
16
23
  }
@@ -0,0 +1,77 @@
1
+ import ok from "../response/ok";
2
+ import type { ParamsOf, SchemaBody, SchemaQuery } from "./ServerEndPoint";
3
+ import type ServerEndPoint from "./ServerEndPoint";
4
+ import type { StandardSchemaV1 } from "./StandardSchema";
5
+
6
+ type Equals<A, B> =
7
+ (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false;
8
+ type Expect<T extends true> = T;
9
+
10
+ // A mock schema library's schema, with input and output types
11
+ declare const postSchema: StandardSchemaV1<{ title: string }, { title: string; upper: string }>;
12
+ declare const querySchema: StandardSchemaV1<{ page: string }, { page: number }>;
13
+ declare const paramsSchema: StandardSchemaV1<{ id: string }, { id: number }>;
14
+
15
+ const schemas: { post: typeof postSchema } = { post: postSchema };
16
+
17
+ // An endpoint declaring a schema for its post handler. The `.upper` access
18
+ // below compiles only when json() is typed from the schema's output
19
+ const endpoint: ServerEndPoint<"/api/posts", typeof schemas> = {
20
+ schema: schemas,
21
+ post: async ({ json }) => ok({ echoed: (await json()).upper }),
22
+ };
23
+
24
+ // json() returns the schema's output type
25
+ type PostEvent = Parameters<NonNullable<typeof endpoint.post>>[0];
26
+ export type R01 = Expect<
27
+ Equals<Awaited<ReturnType<PostEvent["json"]>>, { title: string; upper: string }>
28
+ >;
29
+
30
+ // The schema's input type is available via InferInput
31
+ export type R02 = Expect<Equals<StandardSchemaV1.InferInput<typeof postSchema>, { title: string }>>;
32
+
33
+ // Client callers made with makeApi accept the schema's output as their body
34
+ type PostEndPoint = ServerEndPoint<"/api/posts", typeof schemas>;
35
+ export type R03 = Expect<
36
+ Equals<Parameters<Api<PostEndPoint>["post"]>[0], { title: string; upper: string } | undefined>
37
+ >;
38
+ type Api<E extends object> = import("../nav/api").ApiMethods<E>;
39
+
40
+ // The type helpers behind the handler signatures
41
+ export type R04 = Expect<
42
+ Equals<SchemaBody<typeof schemas, "post">, { title: string; upper: string }>
43
+ >;
44
+ export type R05 = Expect<
45
+ Equals<SchemaQuery<typeof schemas, "get">, Record<string, string | string[]>>
46
+ >;
47
+
48
+ // A get handler's schema types the query string instead of the body
49
+ const getEndpoint: ServerEndPoint<"/api/posts", { get: typeof querySchema }> = {
50
+ schema: { get: querySchema },
51
+ get: async ({ query }) => ok({ page: (await query()).page }),
52
+ };
53
+ type GetEvent = Parameters<NonNullable<typeof getEndpoint.get>>[0];
54
+ export type R06 = Expect<Equals<Awaited<ReturnType<GetEvent["query"]>>, { page: number }>>;
55
+ export type R07 = Expect<Equals<Awaited<ReturnType<GetEvent["json"]>>, unknown>>;
56
+
57
+ // A params schema types (and coerces) the event's params
58
+ const paramsEndpoint: ServerEndPoint<"/api/posts/[id]", { params: typeof paramsSchema }> = {
59
+ schema: { params: paramsSchema },
60
+ get: async ({ params }) => ok({ id: params.id }),
61
+ };
62
+ type ParamsEvent = Parameters<NonNullable<typeof paramsEndpoint.get>>[0];
63
+ export type R08 = Expect<Equals<ParamsEvent["params"], { id: number }>>;
64
+
65
+ // Endpoints without schemas keep the loose types
66
+ const plainEndpoint: ServerEndPoint<"/api/posts"> = {
67
+ post: async ({ json }) => ok({ v: await json() }),
68
+ };
69
+ type PlainEvent = Parameters<NonNullable<typeof plainEndpoint.post>>[0];
70
+ export type R09 = Expect<Equals<Awaited<ReturnType<PlainEvent["json"]>>, unknown>>;
71
+
72
+ // The params of a route without a params schema are string record of the
73
+ // route's dynamic segments
74
+ export type R10 = Expect<
75
+ Equals<ParamsOf<ServerEndPointSchemas, "/api/posts/[id]">, { id: string }>
76
+ >;
77
+ type ServerEndPointSchemas = import("./ServerEndPoint").ServerEndPointSchemas;
@@ -1,37 +1,169 @@
1
+ import type { RouteParamsOf } from "./ParseRouteParams";
2
+ import type { FormDataRecord, QueryRecord } from "./ServerLoadEvent";
3
+ import type { StandardSchemaV1 } from "./StandardSchema";
1
4
  import type ServerRequest from "./ServerRequest";
2
5
 
3
6
  /**
4
- * For +server.
7
+ * Optional standard schemas for +server endpoints, keyed by handler name.
8
+ * Any schema implementing the Standard Schema interface (zod, valibot,
9
+ * arktype, etc) can be used.
10
+ *
11
+ * - `get`/`head` schemas validate the URL's query string, surfaced through
12
+ * `event.query()`
13
+ * - `post`/`patch`/`put`/`del`/`options` schemas validate the JSON request
14
+ * body, surfaced through `event.json()`
15
+ * - a `params` schema validates (and generally coerces) the route params,
16
+ * surfaced through `event.params`
17
+ *
18
+ * When a request body or query string fails validation, the handler is not
19
+ * called and a 422 response with the schema's issues is returned. When route
20
+ * params fail validation, a 404 response is returned, since the URL can't
21
+ * refer to an existing resource.
5
22
  */
6
- type ServerEndPoint = { [key: string]: ServerRequest } & {
23
+ export type ServerEndPointSchemas = {
24
+ params?: StandardSchemaV1;
25
+ get?: StandardSchemaV1;
26
+ post?: StandardSchemaV1;
27
+ patch?: StandardSchemaV1;
28
+ put?: StandardSchemaV1;
29
+ del?: StandardSchemaV1;
30
+ options?: StandardSchemaV1;
31
+ head?: StandardSchemaV1;
32
+ };
33
+
34
+ /**
35
+ * The json body type for a handler, inferred from its schema. Falls back to
36
+ * `unknown` for handlers without a declared schema.
37
+ */
38
+ export type SchemaBody<Schemas extends ServerEndPointSchemas, Method extends string> = [
39
+ Method,
40
+ ] extends [keyof Schemas & string]
41
+ ? Schemas[Method] extends StandardSchemaV1
42
+ ? StandardSchemaV1.InferOutput<Schemas[Method]>
43
+ : unknown
44
+ : unknown;
45
+
46
+ /**
47
+ * The query type for a handler, inferred from its schema. Falls back to a
48
+ * loose record for handlers without a declared schema.
49
+ */
50
+ export type SchemaQuery<Schemas extends ServerEndPointSchemas, Method extends string> = [
51
+ Method,
52
+ ] extends [keyof Schemas & string]
53
+ ? Schemas[Method] extends StandardSchemaV1
54
+ ? StandardSchemaV1.InferOutput<Schemas[Method]>
55
+ : QueryRecord
56
+ : QueryRecord;
57
+
58
+ /**
59
+ * The route params type for an endpoint, inferred from its `params` schema.
60
+ * Falls back to the route's string params when no schema is declared.
61
+ */
62
+ export type ParamsOf<
63
+ Schemas extends { params?: StandardSchemaV1 },
64
+ Route extends string | undefined,
65
+ > = Schemas extends { params: StandardSchemaV1 }
66
+ ? StandardSchemaV1.InferOutput<Schemas["params"]>
67
+ : RouteParamsOf<Route>;
68
+
69
+ /**
70
+ * For +server. Annotate with a route path to get typed params, e.g.
71
+ * `ServerEndPoint<"/api/posts/[id]">`, and with a schemas object to get
72
+ * typed (and validated) request bodies, query strings and params, e.g.
73
+ * `ServerEndPoint<"/api/posts", typeof schema>`.
74
+ */
75
+ type ServerEndPoint<
76
+ Route extends string | undefined = undefined,
77
+ Schemas extends ServerEndPointSchemas = ServerEndPointSchemas,
78
+ > = {
79
+ /**
80
+ * Optional standard schemas for validating requests, keyed by handler
81
+ * name (plus a `params` schema for the route params), e.g.
82
+ *
83
+ * ```ts
84
+ * const schema = {
85
+ * params: z.object({ id: z.coerce.number() }),
86
+ * get: z.object({ sort: z.enum(["asc", "desc"]) }),
87
+ * post: z.object({ title: z.string() }),
88
+ * };
89
+ * export default {
90
+ * schema,
91
+ * get: async (event) => ok(await event.query()),
92
+ * post: async (event) => ok((await event.json()).title),
93
+ * } satisfies ServerEndPoint<"/api/posts/[id]", typeof schema>;
94
+ * ```
95
+ */
96
+ schema?: Schemas | undefined;
7
97
  /**
8
- * Performs a GET.
98
+ * Performs a GET. Its schema validates the query string.
9
99
  */
10
- get?: ServerRequest;
100
+ get?: ServerRequest<
101
+ Route,
102
+ unknown,
103
+ FormDataRecord,
104
+ SchemaQuery<Schemas, "get">,
105
+ ParamsOf<Schemas, Route>
106
+ >;
11
107
  /**
12
- * Performs a POST.
108
+ * Performs a POST. Its schema validates the JSON request body.
13
109
  */
14
- post?: ServerRequest;
110
+ post?: ServerRequest<
111
+ Route,
112
+ SchemaBody<Schemas, "post">,
113
+ FormDataRecord,
114
+ QueryRecord,
115
+ ParamsOf<Schemas, Route>
116
+ >;
15
117
  /**
16
- * Performs a PATCH.
118
+ * Performs a PATCH. Its schema validates the JSON request body.
17
119
  */
18
- patch?: ServerRequest;
120
+ patch?: ServerRequest<
121
+ Route,
122
+ SchemaBody<Schemas, "patch">,
123
+ FormDataRecord,
124
+ QueryRecord,
125
+ ParamsOf<Schemas, Route>
126
+ >;
19
127
  /**
20
- * Performs a PUT.
128
+ * Performs a PUT. Its schema validates the JSON request body.
21
129
  */
22
- put?: ServerRequest;
130
+ put?: ServerRequest<
131
+ Route,
132
+ SchemaBody<Schemas, "put">,
133
+ FormDataRecord,
134
+ QueryRecord,
135
+ ParamsOf<Schemas, Route>
136
+ >;
23
137
  /**
24
- * Performs a DELETE.
138
+ * Performs a DELETE. Its schema validates the JSON request body.
25
139
  */
26
- del?: ServerRequest;
140
+ del?: ServerRequest<
141
+ Route,
142
+ SchemaBody<Schemas, "del">,
143
+ FormDataRecord,
144
+ QueryRecord,
145
+ ParamsOf<Schemas, Route>
146
+ >;
27
147
  /**
28
- * Performs an OPTIONS request.
148
+ * Performs an OPTIONS request. Its schema validates the JSON request body.
29
149
  */
30
- options?: ServerRequest;
150
+ options?: ServerRequest<
151
+ Route,
152
+ SchemaBody<Schemas, "options">,
153
+ FormDataRecord,
154
+ QueryRecord,
155
+ ParamsOf<Schemas, Route>
156
+ >;
31
157
  /**
32
- * Performs a HEAD request.
158
+ * Performs a HEAD request. Its schema validates the query string.
33
159
  */
34
- head?: ServerRequest;
160
+ head?: ServerRequest<
161
+ Route,
162
+ unknown,
163
+ FormDataRecord,
164
+ SchemaQuery<Schemas, "head">,
165
+ ParamsOf<Schemas, Route>
166
+ >;
35
167
  };
36
168
 
37
169
  export default ServerEndPoint;
@@ -3,9 +3,16 @@ import type ServerLoadEvent from "./ServerLoadEvent";
3
3
  /**
4
4
  * For _hook.server.
5
5
  */
6
- export default interface ServerHook {
6
+ export default interface ServerHook<Route extends string | undefined = undefined> {
7
7
  /**
8
- * Called on each server request.
8
+ * Called before each server request is handled. Return a Response to
9
+ * short-circuit the request, skipping the load, action or view rendering
10
+ * (e.g. a redirect for unauthenticated users).
9
11
  */
10
- handle?: (event: ServerLoadEvent) => Promise<void> | void;
12
+ enter?: (event: ServerLoadEvent<Route>) => Promise<Response | void> | Response | void;
13
+ /**
14
+ * Called after the request has been handled, even if the handler threw
15
+ * an error or the enter hook short-circuited it.
16
+ */
17
+ exit?: (event: ServerLoadEvent<Route>) => Promise<void> | void;
11
18
  }
@@ -1,15 +1,43 @@
1
1
  import CookieHelper from "../server/CookieHelper";
2
2
  import HeaderHelper from "../server/HeaderHelper";
3
+ import type { RouteParamsOf } from "./ParseRouteParams";
3
4
 
4
- export default interface ServerLoadEvent {
5
+ /**
6
+ * The values read from a form submission: one value per field, or an array
7
+ * when a field was submitted with multiple values (e.g. a multi select).
8
+ */
9
+ export type FormDataRecord = Record<string, FormDataEntryValue | FormDataEntryValue[]>;
10
+
11
+ /**
12
+ * The values read from a URL's query string: one string per param, or an
13
+ * array when a param was repeated.
14
+ */
15
+ export type QueryRecord = Record<string, string | string[]>;
16
+
17
+ /**
18
+ * The event passed to server functions. Annotate with a route path to get
19
+ * typed params, e.g. `ServerLoadEvent<"/posts/[id]">`, and with a body type
20
+ * to get typed request bodies in API endpoints, e.g.
21
+ * `ServerLoadEvent<"/api/posts", { title: string }>` — which also types the
22
+ * `body` param of client calls made with `makeApi`.
23
+ */
24
+ export default interface ServerLoadEvent<
25
+ Route extends string | undefined = undefined,
26
+ Body = unknown,
27
+ FormBody = FormDataRecord,
28
+ QueryBody = QueryRecord,
29
+ Params = RouteParamsOf<Route>,
30
+ > {
5
31
  /**
6
32
  * The URL for the server function.
7
33
  */
8
34
  url: URL;
9
35
  /**
10
- * Route params from the URL and route path.
36
+ * Route params from the URL and route path. When the endpoint declares a
37
+ * `params` schema, the values are validated and typed by the schema's
38
+ * output.
11
39
  */
12
- params: Record<string, string>;
40
+ params: Params;
13
41
  // TODO: Maybe we should find a better name for the data that is set set in
14
42
  // pages, and just call this data?
15
43
  /**
@@ -22,6 +50,23 @@ export default interface ServerLoadEvent {
22
50
  */
23
51
  request: Request;
24
52
  //response: ServerResponse;
53
+ /**
54
+ * Reads the request body as JSON, typed by the event's `Body` annotation.
55
+ */
56
+ json: () => Promise<Body>;
57
+ /**
58
+ * Reads the request body as form data, returning a record of field values.
59
+ * When the endpoint declares a schema for the form, the values are
60
+ * validated and typed by the schema's output.
61
+ */
62
+ form: () => Promise<FormBody>;
63
+ /**
64
+ * Reads the URL's query string, returning a record of param values. When
65
+ * the endpoint declares a schema for the query (get/head handlers and the
66
+ * load function), the values are validated and typed by the schema's
67
+ * output.
68
+ */
69
+ query: () => Promise<QueryBody>;
25
70
  /**
26
71
  * A helper for getting and setting cookie data.
27
72
  */
@@ -1,7 +1,15 @@
1
+ import type { RouteParamsOf } from "./ParseRouteParams";
2
+ import type { FormDataRecord, QueryRecord } from "./ServerLoadEvent";
1
3
  import type ServerLoadEvent from "./ServerLoadEvent";
2
4
 
3
- type ServerRequest = (
4
- event: ServerLoadEvent,
5
+ type ServerRequest<
6
+ Route extends string | undefined = undefined,
7
+ Body = unknown,
8
+ FormBody = FormDataRecord,
9
+ QueryBody = QueryRecord,
10
+ Params = RouteParamsOf<Route>,
11
+ > = (
12
+ event: ServerLoadEvent<Route, Body, FormBody, QueryBody, Params>,
5
13
  ) => Response | undefined | void | Promise<Response | undefined | void>;
6
14
 
7
15
  export default ServerRequest;
@@ -0,0 +1,24 @@
1
+ import type Site from "../site/Site";
2
+
3
+ /**
4
+ * A Torpor site plugin. A plugin is a function that receives the Site and
5
+ * sets itself up on it, typically by registering routes (e.g. an OpenAPI
6
+ * document endpoint) or adding configuration.
7
+ *
8
+ * Plugins are added to `site.plugins` in the site config file, and are run
9
+ * once when the config is loaded (for dev and build), and once when the
10
+ * server starts up in production:
11
+ *
12
+ * ```ts
13
+ * import { Site } from "@torpor/build";
14
+ * import { openApi } from "@torpor/build/openapi";
15
+ *
16
+ * const site = new Site();
17
+ * site.addRouteFolder("src/routes");
18
+ * site.plugins = [openApi()];
19
+ * export default site;
20
+ * ```
21
+ */
22
+ type SitePlugin = (site: Site) => void | Promise<void>;
23
+
24
+ export default SitePlugin;