@torpor/build 0.4.14 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/README.md +20 -2
  2. package/dist/Jsonify-Dg6gChd3.d.mts +10 -0
  3. package/dist/Jsonify-Dg6gChd3.d.mts.map +1 -0
  4. package/dist/ParseRouteParams-DBmqlmLh.d.mts +42 -0
  5. package/dist/ParseRouteParams-DBmqlmLh.d.mts.map +1 -0
  6. package/dist/{RouteType-B6BMOyYw.mjs → Router-C4ADUfhv.mjs} +30 -42
  7. package/dist/Router-C4ADUfhv.mjs.map +1 -0
  8. package/dist/{Server-CF5wDJp6.d.mts → Server-C_IKav_e.d.mts} +44 -16
  9. package/dist/Server-C_IKav_e.d.mts.map +1 -0
  10. package/dist/{ServerEvent-CxPwA4lH.mjs → ServerEvent-Dljn5i7A.mjs} +37 -11
  11. package/dist/ServerEvent-Dljn5i7A.mjs.map +1 -0
  12. package/dist/Site-CMa153FA.d.mts +565 -0
  13. package/dist/Site-CMa153FA.d.mts.map +1 -0
  14. package/dist/StandardSchema-D28L8_zZ.d.mts +100 -0
  15. package/dist/StandardSchema-D28L8_zZ.d.mts.map +1 -0
  16. package/dist/TypedResponse-6lznqN4U.d.mts +22 -0
  17. package/dist/TypedResponse-6lznqN4U.d.mts.map +1 -0
  18. package/dist/{_page-BQs4WvCO.mjs → _page-4cQKs_Dn.mjs} +3 -5
  19. package/dist/_page-4cQKs_Dn.mjs.map +1 -0
  20. package/dist/bin/index.d.mts +1 -1
  21. package/dist/bin/index.js +15 -11
  22. package/dist/bin/index.js.map +1 -1
  23. package/dist/flattenHeaders-C_YYLdOq.mjs +114 -0
  24. package/dist/flattenHeaders-C_YYLdOq.mjs.map +1 -0
  25. package/dist/form.d.mts +35 -0
  26. package/dist/form.d.mts.map +1 -0
  27. package/dist/form.mjs +48 -0
  28. package/dist/form.mjs.map +1 -0
  29. package/dist/index.d.mts +72 -156
  30. package/dist/index.d.mts.map +1 -1
  31. package/dist/index.mjs +158 -26
  32. package/dist/index.mjs.map +1 -1
  33. package/dist/nav.d.mts +58 -5
  34. package/dist/nav.d.mts.map +1 -1
  35. package/dist/nav.mjs +116 -38
  36. package/dist/nav.mjs.map +1 -1
  37. package/dist/openapi.d.mts +144 -0
  38. package/dist/openapi.d.mts.map +1 -0
  39. package/dist/openapi.mjs +2 -0
  40. package/dist/pathTrie-D4Ax2Mu9.mjs +160 -0
  41. package/dist/pathTrie-D4Ax2Mu9.mjs.map +1 -0
  42. package/dist/plugin-D8Www11B.mjs +202 -0
  43. package/dist/plugin-D8Www11B.mjs.map +1 -0
  44. package/dist/{response-C5TtAsh1.mjs → response-Bg4w2S1q.mjs} +16 -39
  45. package/dist/response-Bg4w2S1q.mjs.map +1 -0
  46. package/dist/response.d.mts +309 -289
  47. package/dist/response.d.mts.map +1 -1
  48. package/dist/response.mjs +3 -4
  49. package/dist/run.d.mts +11 -4
  50. package/dist/run.d.mts.map +1 -1
  51. package/dist/run.mjs +2 -8
  52. package/dist/runOpenApi-BUi1RH1l.mjs +1381 -0
  53. package/dist/runOpenApi-BUi1RH1l.mjs.map +1 -0
  54. package/dist/schema.d.mts +32 -0
  55. package/dist/schema.d.mts.map +1 -0
  56. package/dist/schema.mjs +2 -0
  57. package/dist/server.d.mts +10 -8
  58. package/dist/server.d.mts.map +1 -1
  59. package/dist/server.mjs +132 -4
  60. package/dist/server.mjs.map +1 -0
  61. package/dist/state.d.mts.map +1 -1
  62. package/dist/state.mjs +2 -3
  63. package/dist/test.d.mts +8 -10
  64. package/dist/test.d.mts.map +1 -1
  65. package/dist/test.mjs +283 -79
  66. package/dist/test.mjs.map +1 -1
  67. package/dist/{seeOther-B4Yhu9iq.mjs → unprocessable-DAZbrDeR.mjs} +34 -13
  68. package/dist/unprocessable-DAZbrDeR.mjs.map +1 -0
  69. package/dist/validate-Ok4krlkX.mjs +40 -0
  70. package/dist/validate-Ok4krlkX.mjs.map +1 -0
  71. package/package.json +28 -17
  72. package/src/bin/index.ts +13 -2
  73. package/src/dev.ts +10 -0
  74. package/src/form/formDataToRecord.ts +23 -0
  75. package/src/form/readForm.ts +71 -0
  76. package/src/form.ts +4 -0
  77. package/src/index.ts +53 -1
  78. package/src/nav/api.test-d.ts +79 -0
  79. package/src/nav/api.ts +104 -0
  80. package/src/nav/formSubmit.ts +18 -4
  81. package/src/nav/navigate.ts +40 -15
  82. package/src/nav/route.ts +38 -0
  83. package/src/nav.ts +3 -1
  84. package/src/openapi/docsHtml.ts +25 -0
  85. package/src/openapi/document.ts +154 -0
  86. package/src/openapi/plugin.ts +78 -0
  87. package/src/openapi/types.ts +82 -0
  88. package/src/openapi.ts +14 -0
  89. package/src/response/TypedResponse.ts +17 -0
  90. package/src/response/badRequest.ts +14 -2
  91. package/src/response/created.ts +13 -2
  92. package/src/response/found.ts +2 -2
  93. package/src/response/movedPermanently.ts +2 -2
  94. package/src/response/notModified.ts +2 -2
  95. package/src/response/ok.ts +13 -4
  96. package/src/response/response.ts +4 -4
  97. package/src/response/unprocessable.ts +14 -2
  98. package/src/run/depCache.ts +175 -0
  99. package/src/run/devPlugin.ts +128 -0
  100. package/src/run/prepareTemplate.ts +6 -3
  101. package/src/run/run.ts +63 -39
  102. package/src/run/runBuild.ts +63 -12
  103. package/src/run/runDev.ts +146 -58
  104. package/src/run/runOpenApi.ts +52 -0
  105. package/src/run/runPreview.ts +17 -26
  106. package/src/run/staleTorpCopies.ts +100 -0
  107. package/src/run.ts +2 -1
  108. package/src/schema.ts +7 -0
  109. package/src/server/CookieHelper.ts +17 -7
  110. package/src/server/Server.ts +54 -30
  111. package/src/server/ServerEvent.ts +24 -1
  112. package/src/server/connect/connectMiddleware.ts +44 -40
  113. package/src/server/connect/flattenHeaders.ts +1 -23
  114. package/src/server/connect/requestToNodeMessage.ts +6 -3
  115. package/src/server/types/MiddlewareFunction.ts +18 -4
  116. package/src/site/Router.ts +52 -41
  117. package/src/site/Site.ts +184 -10
  118. package/src/site/checkLayoutSlots.ts +114 -0
  119. package/src/site/checkRoutes.ts +435 -0
  120. package/src/site/clientEntry.ts +22 -9
  121. package/src/site/layoutSlots.ts +53 -0
  122. package/src/site/manifest.ts +125 -6
  123. package/src/site/serverEntry.ts +341 -132
  124. package/src/state/$page.ts +2 -1
  125. package/src/state/$serverPage.ts +22 -0
  126. package/src/test/runTest.ts +306 -118
  127. package/src/types/Adapter.ts +12 -0
  128. package/src/types/Jsonify.ts +20 -0
  129. package/src/types/PageData.test-d.ts +136 -0
  130. package/src/types/PageData.ts +35 -0
  131. package/src/types/PageEndPoint.ts +25 -9
  132. package/src/types/PageForm.test-d.ts +78 -0
  133. package/src/types/PageForm.ts +29 -0
  134. package/src/types/PageLoadEvent.ts +16 -4
  135. package/src/types/PageLoadReturn.ts +13 -0
  136. package/src/types/PageProps.ts +14 -0
  137. package/src/types/PageServerAction.ts +8 -2
  138. package/src/types/PageServerEndPoint.test-d.ts +105 -0
  139. package/src/types/PageServerEndPoint.ts +99 -5
  140. package/src/types/PageServerLoad.ts +11 -3
  141. package/src/types/ParseRouteParams.test-d.ts +84 -0
  142. package/src/types/ParseRouteParams.ts +64 -0
  143. package/src/types/Route.ts +20 -1
  144. package/src/types/RouteHandler.ts +8 -1
  145. package/src/types/ServerEndPoint.test-d.ts +77 -0
  146. package/src/types/ServerEndPoint.ts +148 -16
  147. package/src/types/ServerHook.ts +10 -3
  148. package/src/types/ServerLoadEvent.ts +48 -3
  149. package/src/types/ServerRequest.ts +10 -2
  150. package/src/types/SitePlugin.ts +24 -0
  151. package/src/types/StandardSchema.ts +109 -0
  152. package/src/utils/pathToRegex.ts +4 -2
  153. package/src/utils/pathTrie.ts +182 -0
  154. package/src/utils/searchParamsToRecord.ts +18 -0
  155. package/src/utils/torporPackages.ts +150 -0
  156. package/src/utils/tsconfigAliases.ts +90 -0
  157. package/src/validation/ValidationError.ts +18 -0
  158. package/src/validation/endpoint.ts +83 -0
  159. package/src/validation/validate.ts +26 -0
  160. package/dist/RouteType-B6BMOyYw.mjs.map +0 -1
  161. package/dist/Server-CF5wDJp6.d.mts.map +0 -1
  162. package/dist/ServerEvent-CxPwA4lH.mjs.map +0 -1
  163. package/dist/Site-DgC6WWn1.d.mts +0 -78
  164. package/dist/Site-DgC6WWn1.d.mts.map +0 -1
  165. package/dist/_page-BQs4WvCO.mjs.map +0 -1
  166. package/dist/connectMiddleware-D7nehjj3.mjs +0 -252
  167. package/dist/connectMiddleware-D7nehjj3.mjs.map +0 -1
  168. package/dist/pathToRegex-TUAMc3zS.mjs +0 -11
  169. package/dist/pathToRegex-TUAMc3zS.mjs.map +0 -1
  170. package/dist/response-C5TtAsh1.mjs.map +0 -1
  171. package/dist/run-BPFtSYA3.mjs +0 -332
  172. package/dist/run-BPFtSYA3.mjs.map +0 -1
  173. package/dist/seeOther-B4Yhu9iq.mjs.map +0 -1
  174. package/src/server/Routerx.ts +0 -72
  175. package/src/server/connect/bufferToArrayBuffer.ts +0 -8
  176. package/src/server/connect/readableToBuffer.ts +0 -16
@@ -0,0 +1,136 @@
1
+ import found from "../response/found";
2
+ import ok from "../response/ok";
3
+ import type TypedResponse from "../response/TypedResponse";
4
+ import type { MergePageData, PageData } from "./PageData";
5
+ import type PageEndPoint from "./PageEndPoint";
6
+ import type PageLoadEvent from "./PageLoadEvent";
7
+ import type PageProps from "./PageProps";
8
+ import type PageServerEndPoint from "./PageServerEndPoint";
9
+
10
+ type Equals<A, B> =
11
+ (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false;
12
+ type Expect<T extends true> = T;
13
+
14
+ interface Post {
15
+ title: string;
16
+ }
17
+
18
+ // A server endpoint whose load returns typed data
19
+ interface PostsServer {
20
+ load: () => Promise<TypedResponse<{ posts: { title: string }[] }>>;
21
+ }
22
+
23
+ export const server: PostsServer = {
24
+ load: async () => ok({ posts: [{ title: "Hello" }] }),
25
+ };
26
+
27
+ // The shape above is a valid +page.server endpoint
28
+ export const serverCompat: PageServerEndPoint<"/posts"> = server;
29
+
30
+ // PageData extracts the Jsonify'd body of the load's typed responses
31
+ export type D01 = Expect<Equals<PageData<PostsServer>, { posts: { title: string }[] }>>;
32
+
33
+ // Dates are Jsonify'd to strings through ok()
34
+ interface ClockServer {
35
+ load: () => Promise<TypedResponse<{ now: string }>>;
36
+ }
37
+
38
+ export const clockServer: ClockServer = {
39
+ load: async () => ok({ now: new Date() }),
40
+ };
41
+
42
+ export type D02 = Expect<Equals<PageData<ClockServer>, { now: string }>>;
43
+
44
+ // Endpoints without a load fall back to a loose record
45
+ export const actionsOnly: PageServerEndPoint<"/posts"> = {
46
+ actions: { default: async () => ok({}) },
47
+ };
48
+ export type D03 = Expect<Equals<PageData<typeof actionsOnly>, Record<string, any>>>;
49
+
50
+ // Untyped loads (redirects etc) fall back to a loose record
51
+ export const untypedLoad: PageServerEndPoint<"/posts"> = {
52
+ load: async () => found("/elsewhere"),
53
+ };
54
+ export type D04 = Expect<Equals<PageData<typeof untypedLoad>, Record<string, any>>>;
55
+
56
+ // The page endpoint: data keys are checked against Data
57
+ export const page: PageEndPoint<"/posts", PageData<PostsServer>> = {
58
+ load: async () => ok({ posts: [{ title: "Hello" }] }),
59
+ };
60
+
61
+ export const badPage: PageEndPoint<"/posts", PageData<PostsServer>> = {
62
+ // @ts-expect-error 'comments' is not part of the page's data
63
+ load: async () => ok({ comments: [] }),
64
+ };
65
+
66
+ // Untyped responses (redirects etc) are always allowed
67
+ export const redirecting: PageEndPoint<"/posts", PageData<PostsServer>> = {
68
+ load: async () => found("/elsewhere"),
69
+ };
70
+
71
+ // Loads with no return are allowed
72
+ export const silent: PageEndPoint<"/posts", PageData<PostsServer>> = {
73
+ load: async () => {},
74
+ };
75
+
76
+ // Server endpoints are checked the same way
77
+ export const checkedServer: PageServerEndPoint<"/posts", { posts: Post[] }> = {
78
+ load: async () => ok({ posts: [{ title: "Hello" }] }),
79
+ };
80
+
81
+ export const badServer: PageServerEndPoint<"/posts", { posts: Post[] }> = {
82
+ // @ts-expect-error wrong data shape is flagged on the server endpoint too
83
+ load: async () => ok({ posts: "not an array" }),
84
+ };
85
+
86
+ // PageProps: the shape page components receive
87
+ const props: PageProps<PageData<PostsServer>> = null as never;
88
+ export const posts: { title: string }[] = props.data.posts;
89
+ export const form: Record<string, any> | undefined = props.form;
90
+
91
+ // --- Layout data ---
92
+
93
+ // A layout endpoint with its own typed load
94
+ interface LayoutServer {
95
+ load: () => Promise<TypedResponse<{ user: { name: string } }>>;
96
+ }
97
+
98
+ export const layoutServer: LayoutServer = {
99
+ load: async () => ok({ user: { name: "Andrew" } }),
100
+ };
101
+
102
+ // MergePageData intersects layout and page data, in order
103
+ export type M01 = Expect<
104
+ Equals<
105
+ MergePageData<[typeof layoutServer, PostsServer]>,
106
+ { user: { name: string } } & { posts: { title: string }[] }
107
+ >
108
+ >;
109
+
110
+ // A single endpoint merges to its own data
111
+ export type M02 = Expect<Equals<MergePageData<[PostsServer]>, { posts: { title: string }[] }>>;
112
+
113
+ // The merged shape is what the page component receives as $props.data
114
+ const mergedProps: PageProps<MergePageData<[typeof layoutServer, PostsServer]>> = null as never;
115
+ export const userName: string = mergedProps.data.user.name;
116
+ export const mergedPosts: { title: string }[] = mergedProps.data.posts;
117
+
118
+ // Endpoints without a typed load contribute only looseness
119
+ const looseProps: PageProps<MergePageData<[typeof actionsOnly, PostsServer]>> = null as never;
120
+ export const loosePosts: { title: string }[] = looseProps.data.posts;
121
+
122
+ // --- Client load events ---
123
+
124
+ // event.data is typed by the second annotation, e.g. the layout's data
125
+ export const clientLoad = (event: PageLoadEvent<"/posts", PageData<typeof layoutServer>>): void =>
126
+ void event.data.user.name;
127
+
128
+ // 'nope' is not part of the accumulated layout data
129
+ export const badClientLoad = (
130
+ event: PageLoadEvent<"/posts", PageData<typeof layoutServer>>,
131
+ ): void =>
132
+ // @ts-expect-error 'nope' is not part of the accumulated layout data
133
+ void event.data.nope;
134
+
135
+ // Without a data annotation it stays loose
136
+ export const looseClientLoad = (event: PageLoadEvent<"/posts">): void => void event.data.whatever;
@@ -0,0 +1,35 @@
1
+ import type TypedResponse from "../response/TypedResponse";
2
+
3
+ /**
4
+ * The page data produced by an endpoint's `load` function, e.g.
5
+ *
6
+ * ```ts
7
+ * import type server from "./+page.server";
8
+ * type Data = PageData<typeof server>;
9
+ * ```
10
+ *
11
+ * Falls back to a loose record when the endpoint has no load or its load
12
+ * returns untyped responses.
13
+ */
14
+ export type PageData<EndPoint> = EndPoint extends { load?: (event: any) => infer Return }
15
+ ? NonNullable<Awaited<Return>> extends TypedResponse<infer Body>
16
+ ? Body
17
+ : Record<string, any>
18
+ : Record<string, any>;
19
+
20
+ /**
21
+ * The merged page data for a page that sits under one or more layouts: pass
22
+ * the layout endpoints first and the page endpoint last, e.g.
23
+ *
24
+ * ```ts
25
+ * import type layoutServer from "../_layout.server";
26
+ * import type server from "./+page.server";
27
+ * type Data = MergePageData<typeof layoutServer, typeof server>;
28
+ * ```
29
+ *
30
+ * Each endpoint's data is intersected in order; endpoints without a typed
31
+ * `load` contribute nothing but looseness.
32
+ */
33
+ export type MergePageData<EndPoints extends any[]> = EndPoints extends [infer First, ...infer Rest]
34
+ ? PageData<First> & MergePageData<Rest>
35
+ : unknown;
@@ -1,21 +1,37 @@
1
1
  import { type Component } from "@torpor/view";
2
+ import type { ExactRouteParams, ParseRouteParams, RouteParamsOf } from "./ParseRouteParams";
2
3
  import type PageLoadEvent from "./PageLoadEvent";
4
+ import type { PageLoadReturn } from "./PageLoadReturn";
3
5
 
4
6
  /**
5
- * For +page.
7
+ * For +page. Annotate with a route path to get typed params, and with a data
8
+ * shape to flag loads that return keys the page doesn't expect, e.g.
9
+ * `PageEndPoint<"/posts/[id]", PageData<typeof server>>`.
6
10
  */
7
- export default interface PageEndPoint {
11
+ export default interface PageEndPoint<
12
+ Route extends string | undefined = undefined,
13
+ Data = Record<string, any>,
14
+ > {
8
15
  /**
9
- * Builds the route path for the page in a type-safe manner.
10
- * TODO: Not sure this is actually the best way to do it...
16
+ * Builds the route path for the page in a type-safe manner. The params
17
+ * object is checked for exact keys when `Route` is annotated.
18
+ *
19
+ * NOTE: The rest args conditional must stay INLINE — routing it through a
20
+ * type alias defeats `Params` inference, and excess keys stop being checked
11
21
  */
12
- route?: (...args: any[]) => string;
22
+ route?: <Params extends RouteParamsOf<Route>>(
23
+ ...args: Route extends string
24
+ ? string extends Route
25
+ ? [params?: Record<string, string>]
26
+ : keyof ParseRouteParams<Route> extends never
27
+ ? []
28
+ : [params: Params & ExactRouteParams<Params, ParseRouteParams<Route>>]
29
+ : [params?: Record<string, string>]
30
+ ) => string;
13
31
  /**
14
32
  * Loads data for the page.
15
33
  */
16
- load?: (
17
- event: PageLoadEvent,
18
- ) => Response | undefined | void | Promise<Response | undefined | void>;
34
+ load?: (event: PageLoadEvent<Route>) => PageLoadReturn<Data> | Promise<PageLoadReturn<Data>>;
19
35
  /**
20
36
  * The component that is displayed for the page.
21
37
  */
@@ -24,7 +40,7 @@ export default interface PageEndPoint {
24
40
  /**
25
41
  * The head element data for the page, which may include a <title> and <meta> elements.
26
42
  */
27
- head?: HeadElement[] | ((event: PageLoadEvent) => HeadElement[]);
43
+ head?: HeadElement[] | ((event: PageLoadEvent<Route>) => HeadElement[]);
28
44
  }
29
45
 
30
46
  type HeadElement = TitleElement | MetaElement;
@@ -0,0 +1,78 @@
1
+ import badRequest from "../response/badRequest";
2
+ import ok from "../response/ok";
3
+ import type TypedResponse from "../response/TypedResponse";
4
+ import type { PageData } from "./PageData";
5
+ import type { PageForm } from "./PageForm";
6
+ import type PageProps from "./PageProps";
7
+ import type PageServerEndPoint from "./PageServerEndPoint";
8
+
9
+ type Equals<A, B> =
10
+ (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false;
11
+ type Expect<T extends true> = T;
12
+
13
+ // The docs-site form pattern: ok for success, badRequest for validation errors
14
+ interface FormServer {
15
+ actions: {
16
+ default: () => Promise<TypedResponse<{ ok: true } | { ok: false; message: string }>>;
17
+ };
18
+ }
19
+
20
+ export const formServer: FormServer = {
21
+ actions: {
22
+ default: async () =>
23
+ Math.random() > 0.5
24
+ ? ok({ ok: true as const })
25
+ : badRequest({ ok: false as const, message: "bad text" }),
26
+ },
27
+ };
28
+
29
+ // The shape above is a valid +page.server endpoint
30
+ export const formServerCompat: PageServerEndPoint<"/form"> = formServer;
31
+
32
+ // PageForm extracts the union of action result bodies
33
+ export type F01 = Expect<
34
+ Equals<PageForm<FormServer>, { ok: true } | { ok: false; message: string }>
35
+ >;
36
+
37
+ // PageProps carries data and form together
38
+ const props: PageProps<PageData<FormServer>, PageForm<FormServer>> = null as never;
39
+ const form = props.form;
40
+ export const okValue: boolean | undefined = form?.ok;
41
+ export const message: string = form && form.ok === false ? form.message : "";
42
+
43
+ // badRequest bodies are typed and Jsonify'd
44
+ export const bad: TypedResponse<{ ok: false; message: string }> = badRequest({
45
+ ok: false,
46
+ message: "bad text",
47
+ });
48
+ export const untypedBad: Response = badRequest("plain");
49
+
50
+ // Endpoints without actions fall back to a loose record
51
+ interface LoadOnly {
52
+ load: () => Promise<TypedResponse<{ posts: { title: string }[] }>>;
53
+ }
54
+ export type F02 = Expect<Equals<PageForm<LoadOnly>, Record<string, any>>>;
55
+
56
+ // Annotated (widened) endpoints fall back to a loose record
57
+ export const widened: PageServerEndPoint<"/form"> = {
58
+ actions: { default: async () => ok({ ok: true }) },
59
+ };
60
+ export type F03 = Expect<Equals<PageForm<typeof widened>, Record<string, any>>>;
61
+
62
+ // Multiple actions union their bodies
63
+ interface MultiServer {
64
+ actions: {
65
+ save: () => Promise<TypedResponse<{ saved: true }>>;
66
+ remove: () => Promise<TypedResponse<{ removed: true }>>;
67
+ };
68
+ }
69
+ export type F04 = Expect<Equals<PageForm<MultiServer>, { saved: true } | { removed: true }>>;
70
+
71
+ // Mixed typed and untyped action returns fall back to a loose record
72
+ interface MixedServer {
73
+ actions: {
74
+ save: () => Promise<TypedResponse<{ saved: true }>>;
75
+ other: () => Promise<Response>;
76
+ };
77
+ }
78
+ export type F05 = Expect<Equals<PageForm<MixedServer>, Record<string, any>>>;
@@ -0,0 +1,29 @@
1
+ import type TypedResponse from "../response/TypedResponse";
2
+
3
+ /** The non-undefined return values of an endpoint's actions. */
4
+ type ActionReturns<Actions> = {
5
+ [K in keyof Actions]: Actions[K] extends (event: any) => infer Return
6
+ ? NonNullable<Awaited<Return>>
7
+ : never;
8
+ }[keyof Actions];
9
+
10
+ /**
11
+ * The form result an endpoint's actions produce, i.e. what becomes the page's
12
+ * `$props.form` after a submit, e.g.
13
+ *
14
+ * ```ts
15
+ * import type server from "./+page.server";
16
+ * type Form = PageForm<typeof server>;
17
+ * ```
18
+ *
19
+ * Actions returning typed responses (e.g. `ok({ ... })`, `badRequest({ ... })`)
20
+ * contribute their JSON bodies as a union. Falls back to a loose record when
21
+ * there are no actions or their responses are untyped.
22
+ */
23
+ export type PageForm<EndPoint> = EndPoint extends { actions?: infer Actions }
24
+ ? [NonNullable<Actions>] extends [never]
25
+ ? Record<string, any>
26
+ : ActionReturns<NonNullable<Actions>> extends TypedResponse<infer Body>
27
+ ? Body
28
+ : Record<string, any>
29
+ : Record<string, any>;
@@ -1,4 +1,15 @@
1
- export default interface PageLoadEvent {
1
+ import type { RouteParamsOf } from "./ParseRouteParams";
2
+
3
+ /**
4
+ * The event passed to client load functions. Annotate with a route path to
5
+ * get typed params, e.g. `PageLoadEvent<"/posts/[id]">`, and with a data type
6
+ * to type the data accumulated from the layouts above, e.g.
7
+ * `PageLoadEvent<"/posts/[id]", PageData<typeof layoutServer>>`.
8
+ */
9
+ export default interface PageLoadEvent<
10
+ Route extends string | undefined = undefined,
11
+ Data = Record<string, any>,
12
+ > {
2
13
  /**
3
14
  * The URL for the server function.
4
15
  */
@@ -6,9 +17,10 @@ export default interface PageLoadEvent {
6
17
  /**
7
18
  * Route params from the URL and route path.
8
19
  */
9
- params: Record<string, string>;
20
+ params: RouteParamsOf<Route>;
10
21
  /**
11
- * Data that is (optionally) loaded from the load function and passed into the page as $props.data.
22
+ * Data loaded by the layouts above, accumulated top down. Loads may add to
23
+ * it, and the merged result is passed into the page as `$props.data`.
12
24
  */
13
- data: Record<string, any>;
25
+ data: Data;
14
26
  }
@@ -0,0 +1,13 @@
1
+ import type TypedResponse from "../response/TypedResponse";
2
+ import type { UntypedResponse } from "../response/TypedResponse";
3
+
4
+ /**
5
+ * What a load function may return. When `Data` is provided, a typed response
6
+ * (e.g. `ok({ ... })`) must only contain keys the page expects; untyped
7
+ * responses (redirects, errors, plain text) are always allowed.
8
+ */
9
+ export type PageLoadReturn<Data> =
10
+ | TypedResponse<Partial<Data>>
11
+ | UntypedResponse
12
+ | undefined
13
+ | void;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The props passed to a page (or layout) component: the data accumulated from
3
+ * load functions, plus the result of the last form action if any.
4
+ */
5
+ export default interface PageProps<Data = Record<string, any>, Form = Record<string, any>> {
6
+ /**
7
+ * Data loaded by the page's (and its layouts') load functions.
8
+ */
9
+ data: Data;
10
+ /**
11
+ * The JSON result of the last form action, if a form was submitted.
12
+ */
13
+ form: Form | undefined;
14
+ }
@@ -1,7 +1,13 @@
1
+ import type { RouteParamsOf } from "./ParseRouteParams";
2
+ import type { FormDataRecord, QueryRecord } from "./ServerLoadEvent";
1
3
  import type ServerLoadEvent from "./ServerLoadEvent";
2
4
 
3
- type PageServerAction = (
4
- event: ServerLoadEvent,
5
+ type PageServerAction<
6
+ Route extends string | undefined = undefined,
7
+ FormBody = FormDataRecord,
8
+ Params = RouteParamsOf<Route>,
9
+ > = (
10
+ event: ServerLoadEvent<Route, unknown, FormBody, QueryRecord, Params>,
5
11
  ) => Response | undefined | void | Promise<Response | undefined | void>;
6
12
 
7
13
  export default PageServerAction;
@@ -0,0 +1,105 @@
1
+ import ok from "../response/ok";
2
+ import type {
3
+ ActionBody,
4
+ LoadParams,
5
+ LoadQuery,
6
+ PageServerActionSchemas,
7
+ } from "./PageServerEndPoint";
8
+ import type PageServerEndPoint from "./PageServerEndPoint";
9
+ import type { StandardSchemaV1 } from "./StandardSchema";
10
+
11
+ type Equals<A, B> =
12
+ (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false;
13
+ type Expect<T extends true> = T;
14
+
15
+ // A mock schema library's schema, with input and output types
16
+ declare const postSchema: StandardSchemaV1<{ title: string }, { title: string; upper: string }>;
17
+ declare const querySchema: StandardSchemaV1<{ page: string }, { page: number }>;
18
+ declare const paramsSchema: StandardSchemaV1<{ id: string }, { id: number }>;
19
+
20
+ const schemas: { default: typeof postSchema } = { default: postSchema };
21
+
22
+ // An endpoint declaring a schema for its default action. The `.upper` access
23
+ // below compiles only when form() is contextually typed from the schema's
24
+ // output, since actions map their body to their schema's key
25
+ export const endPoint: PageServerEndPoint<"/form", Record<string, any>, typeof schemas> = {
26
+ schema: schemas,
27
+ actions: {
28
+ default: async ({ form }) => ok({ title: (await form()).upper }),
29
+ },
30
+ };
31
+
32
+ // The type helpers behind the load/action signatures
33
+ export type A01 = Expect<
34
+ Equals<ActionBody<typeof schemas, "default">, { title: string; upper: string }>
35
+ >;
36
+ export type A02 = Expect<
37
+ Equals<
38
+ ActionBody<typeof schemas, "missing">,
39
+ Record<string, FormDataEntryValue | FormDataEntryValue[]>
40
+ >
41
+ >;
42
+ export type A03 = Expect<
43
+ Equals<
44
+ ActionBody<PageServerActionSchemas, "default">,
45
+ Record<string, FormDataEntryValue | FormDataEntryValue[]>
46
+ >
47
+ >;
48
+
49
+ // The load function's schema (the `load` key) types the query string
50
+ const querySchemas: { load: typeof querySchema } = { load: querySchema };
51
+ export type A04 = Expect<Equals<LoadQuery<typeof querySchemas>, { page: number }>>;
52
+ export type A05 = Expect<
53
+ Equals<LoadQuery<PageServerActionSchemas>, Record<string, string | string[]>>
54
+ >;
55
+
56
+ // The `params` key types the params of the load function and actions
57
+ const paramSchemas: { params: typeof paramsSchema } = { params: paramsSchema };
58
+ export type A06 = Expect<Equals<LoadParams<typeof paramSchemas, "/posts/[id]">, { id: number }>>;
59
+ export type A07 = Expect<
60
+ Equals<LoadParams<PageServerActionSchemas, "/posts/[id]">, { id: string }>
61
+ >;
62
+
63
+ // An endpoint using all of them: the `page` access below compiles only when
64
+ // the load's query is typed from its schema
65
+ export const fullEndPoint: PageServerEndPoint<
66
+ "/posts/[id]",
67
+ Record<string, any>,
68
+ { params: typeof paramsSchema; load: typeof querySchema; default: typeof postSchema }
69
+ > = {
70
+ schema: {
71
+ params: paramsSchema,
72
+ load: querySchema,
73
+ default: postSchema,
74
+ },
75
+ load: async ({ params, query }) => ok({ id: params.id, page: (await query()).page }),
76
+ actions: {
77
+ default: async ({ params, form }) => ok({ id: params.id, title: (await form()).upper }),
78
+ },
79
+ };
80
+
81
+ // An untyped action keeps the loose form record and string params
82
+ type PlainAction = NonNullable<NonNullable<typeof plainEndPoint.actions>["default"]>;
83
+ const plainEndPoint: PageServerEndPoint<"/form"> = {
84
+ actions: {
85
+ default: async ({ form }) => ok({ title: (await form()).title }),
86
+ },
87
+ };
88
+ type PlainEvent = Parameters<PlainAction>[0];
89
+ export type A08 = Expect<
90
+ Equals<
91
+ Awaited<ReturnType<PlainEvent["form"]>>,
92
+ Record<string, FormDataEntryValue | FormDataEntryValue[]>
93
+ >
94
+ >;
95
+
96
+ // Typed ok() bodies still flow through actions (PageForm's source)
97
+ type DefaultReturn = Awaited<
98
+ ReturnType<NonNullable<NonNullable<typeof endPoint.actions>["default"]>>
99
+ >;
100
+ export type A09 = Expect<Equals<DefaultReturn, Response | undefined | void>>;
101
+
102
+ // The schema property is optional, as before
103
+ export type A10 = Expect<
104
+ Equals<PageServerEndPoint extends { schema?: unknown } ? true : false, true>
105
+ >;
@@ -1,16 +1,110 @@
1
+ import type { RouteParamsOf } from "./ParseRouteParams";
1
2
  import type PageServerAction from "./PageServerAction";
2
3
  import type PageServerLoad from "./PageServerLoad";
4
+ import type { FormDataRecord, QueryRecord } from "./ServerLoadEvent";
5
+ import type { StandardSchemaV1 } from "./StandardSchema";
3
6
 
4
7
  /**
5
- * For +page.server.
8
+ * Optional standard schemas for +page.server endpoints, keyed by name. Any
9
+ * schema implementing the Standard Schema interface (zod, valibot, arktype,
10
+ * etc) can be used.
11
+ *
12
+ * - action name schemas validate the submitted form data, surfaced through
13
+ * `event.form()` in the action
14
+ * - the `load` key validates the URL's query string, surfaced through
15
+ * `event.query()` in the load function
16
+ * - a `params` key validates (and generally coerces) the route params,
17
+ * surfaced through `event.params` in the load function and actions
18
+ *
19
+ * When a submitted form or query string fails validation, the action or load
20
+ * function is not called and a 422 response with the schema's issues is
21
+ * returned (which becomes the page's `$props.form` for actions). When route
22
+ * params fail validation, a 404 response is returned, since the URL can't
23
+ * refer to an existing resource.
6
24
  */
7
- export default interface PageServerEndPoint {
25
+ export type PageServerActionSchemas = {
26
+ params?: StandardSchemaV1;
27
+ load?: StandardSchemaV1;
28
+ [action: string]: StandardSchemaV1 | undefined;
29
+ };
30
+
31
+ /**
32
+ * The form values type for an action, inferred from its schema. Falls back
33
+ * to a loose record for actions without a declared schema.
34
+ */
35
+ export type ActionBody<Schemas extends PageServerActionSchemas, Name extends string> = [
36
+ Name,
37
+ ] extends [keyof Schemas & string]
38
+ ? Schemas[Name] extends StandardSchemaV1
39
+ ? StandardSchemaV1.InferOutput<Schemas[Name]>
40
+ : FormDataRecord
41
+ : FormDataRecord;
42
+
43
+ /**
44
+ * The query type for the load function, inferred from its schema.
45
+ */
46
+ export type LoadQuery<Schemas extends PageServerActionSchemas> = Schemas extends {
47
+ load: StandardSchemaV1;
48
+ }
49
+ ? StandardSchemaV1.InferOutput<Schemas["load"]>
50
+ : QueryRecord;
51
+
52
+ /**
53
+ * The route params type, inferred from the `params` schema.
54
+ */
55
+ export type LoadParams<
56
+ Schemas extends PageServerActionSchemas,
57
+ Route extends string | undefined,
58
+ > = Schemas extends { params: StandardSchemaV1 }
59
+ ? StandardSchemaV1.InferOutput<Schemas["params"]>
60
+ : RouteParamsOf<Route>;
61
+
62
+ /**
63
+ * For +page.server. Annotate with a route path to get typed params, and with
64
+ * a data shape to flag loads that return keys the page doesn't expect, e.g.
65
+ * `PageServerEndPoint<"/posts/[id]", { posts: Post[] }>`. Annotate with a
66
+ * schemas object to get typed (and validated) form values, query strings and
67
+ * params, e.g. `PageServerEndPoint<"/posts", Record<string, any>, typeof schema>`.
68
+ */
69
+ export default interface PageServerEndPoint<
70
+ Route extends string | undefined = undefined,
71
+ Data = Record<string, any>,
72
+ Schemas extends PageServerActionSchemas = PageServerActionSchemas,
73
+ > {
8
74
  /**
9
- * Loads data from the server for a page.
75
+ * Loads data from the server for a page. Its schema (the `load` key)
76
+ * validates the URL's query string.
10
77
  */
11
- load?: PageServerLoad;
78
+ load?: PageServerLoad<Route, Data, LoadQuery<Schemas>, LoadParams<Schemas, Route>>;
12
79
  /**
13
80
  * A map of actions that can be performed on the server for a page, generally from a form submit.
14
81
  */
15
- actions?: Record<string, PageServerAction>;
82
+ actions?: {
83
+ [Name in string]: PageServerAction<
84
+ Route,
85
+ ActionBody<Schemas, Name>,
86
+ LoadParams<Schemas, Route>
87
+ >;
88
+ };
89
+ /**
90
+ * Optional standard schemas for validating requests, keyed by action name
91
+ * (plus `load` for the query string and `params` for the route params),
92
+ * e.g.
93
+ *
94
+ * ```ts
95
+ * const schema = {
96
+ * params: z.object({ id: z.coerce.number() }),
97
+ * load: z.object({ page: z.coerce.number() }),
98
+ * default: z.object({ title: z.string() }),
99
+ * };
100
+ * export default {
101
+ * schema,
102
+ * load: async ({ query }) => ok(await query()),
103
+ * actions: {
104
+ * default: async ({ form }) => ok((await form()).title),
105
+ * },
106
+ * } satisfies PageServerEndPoint<"/posts/[id]", Record<string, any>, typeof schema>;
107
+ * ```
108
+ */
109
+ schema?: Schemas | undefined;
16
110
  }
@@ -1,7 +1,15 @@
1
+ import type { RouteParamsOf } from "./ParseRouteParams";
2
+ import type { PageLoadReturn } from "./PageLoadReturn";
3
+ import type { FormDataRecord, QueryRecord } from "./ServerLoadEvent";
1
4
  import type ServerLoadEvent from "./ServerLoadEvent";
2
5
 
3
- type PageServerLoad = (
4
- event: ServerLoadEvent,
5
- ) => Response | undefined | void | Promise<Response | undefined | void>;
6
+ type PageServerLoad<
7
+ Route extends string | undefined = undefined,
8
+ Data = Record<string, any>,
9
+ QueryBody = QueryRecord,
10
+ Params = RouteParamsOf<Route>,
11
+ > = (
12
+ event: ServerLoadEvent<Route, unknown, FormDataRecord, QueryBody, Params>,
13
+ ) => PageLoadReturn<Data> | Promise<PageLoadReturn<Data>>;
6
14
 
7
15
  export default PageServerLoad;