@torpor/build 0.1.34 → 0.3.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 (164) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/RouteType-g8Ce5gOd.js +107 -0
  3. package/dist/RouteType-g8Ce5gOd.js.map +1 -0
  4. package/dist/Server-CSpY_-Ba.d.ts +78 -0
  5. package/dist/Server-CSpY_-Ba.d.ts.map +1 -0
  6. package/dist/ServerEvent-2-CnAy1W.js +74 -0
  7. package/dist/ServerEvent-2-CnAy1W.js.map +1 -0
  8. package/dist/Site-D4x1l0p5.d.ts +67 -0
  9. package/dist/Site-D4x1l0p5.d.ts.map +1 -0
  10. package/dist/_page-CsdPubw4.js +33 -0
  11. package/dist/_page-CsdPubw4.js.map +1 -0
  12. package/dist/bin/index.d.ts +1 -1
  13. package/dist/bin/index.js +14 -19
  14. package/dist/bin/index.js.map +1 -1
  15. package/dist/connectMiddleware-DbLj0St1.js +252 -0
  16. package/dist/connectMiddleware-DbLj0St1.js.map +1 -0
  17. package/dist/index.d.ts +191 -182
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +109 -145
  20. package/dist/index.js.map +1 -1
  21. package/dist/nav.d.ts +14 -0
  22. package/dist/nav.d.ts.map +1 -0
  23. package/dist/nav.js +184 -0
  24. package/dist/nav.js.map +1 -0
  25. package/dist/pathToRegex-CTgx7hQP.js +11 -0
  26. package/dist/pathToRegex-CTgx7hQP.js.map +1 -0
  27. package/dist/response-D-XpIuMH.js +307 -0
  28. package/dist/response-D-XpIuMH.js.map +1 -0
  29. package/dist/response.d.ts +336 -151
  30. package/dist/response.d.ts.map +1 -0
  31. package/dist/response.js +4 -37
  32. package/dist/run-CNdaKSPR.js +331 -0
  33. package/dist/run-CNdaKSPR.js.map +1 -0
  34. package/dist/run.d.ts +11 -8
  35. package/dist/run.d.ts.map +1 -0
  36. package/dist/run.js +8 -18
  37. package/dist/seeOther-BmehiI4C.js +110 -0
  38. package/dist/seeOther-BmehiI4C.js.map +1 -0
  39. package/dist/server.d.ts +12 -11
  40. package/dist/server.d.ts.map +1 -0
  41. package/dist/server.js +5 -18
  42. package/dist/state.d.ts +14 -10
  43. package/dist/state.d.ts.map +1 -0
  44. package/dist/state.js +3 -7
  45. package/dist/test.d.ts +12 -11
  46. package/dist/test.d.ts.map +1 -0
  47. package/dist/test.js +152 -225
  48. package/dist/test.js.map +1 -1
  49. package/package.json +25 -29
  50. package/src/bin/index.ts +9 -11
  51. package/src/index.ts +3 -0
  52. package/src/nav/load.ts +9 -0
  53. package/src/nav/loadData.ts +121 -0
  54. package/src/nav/navigate.ts +154 -0
  55. package/src/nav/reload.ts +9 -0
  56. package/src/nav.ts +4 -0
  57. package/src/response/badRequest.ts +9 -4
  58. package/src/response/created.ts +10 -3
  59. package/src/response/forbidden.ts +11 -4
  60. package/src/response/found.ts +30 -0
  61. package/src/response/methodNotAllowed.ts +8 -4
  62. package/src/response/movedPermanently.ts +25 -0
  63. package/src/response/notFound.ts +14 -7
  64. package/src/response/notModified.ts +24 -4
  65. package/src/response/ok.ts +19 -10
  66. package/src/response/permanentRedirect.ts +30 -0
  67. package/src/response/seeOther.ts +9 -3
  68. package/src/response/serverError.ts +14 -2
  69. package/src/response/temporaryRedirect.ts +31 -0
  70. package/src/response/unauthorized.ts +11 -4
  71. package/src/response/unprocessable.ts +8 -2
  72. package/src/response.ts +8 -4
  73. package/src/run/prepareTemplate.ts +32 -0
  74. package/src/{site → run}/run.ts +1 -1
  75. package/src/{site → run}/runBuild.ts +2 -2
  76. package/src/{site → run}/runDev.ts +2 -2
  77. package/src/{site → run}/runPreview.ts +12 -2
  78. package/src/run.ts +4 -4
  79. package/src/server/Routerx.ts +3 -3
  80. package/src/server/Server.ts +3 -3
  81. package/src/server/ServerEvent.ts +1 -1
  82. package/src/server/connect/bufferToArrayBuffer.ts +8 -0
  83. package/src/server/connect/connectMiddleware.ts +4 -2
  84. package/src/server/connect/nodeMessageToNodeResponse.ts +2 -2
  85. package/src/server/types/ServerFunction.ts +3 -1
  86. package/src/site/Router.ts +3 -3
  87. package/src/site/clientEntry.ts +40 -273
  88. package/src/site/defaultAdapter.ts +1 -1
  89. package/src/site/serverEntry.ts +86 -50
  90. package/src/state/$page.ts +3 -12
  91. package/src/state/client.ts +6 -0
  92. package/src/state/internal.ts +45 -0
  93. package/src/test/runTest.ts +4 -4
  94. package/src/types/Adapter.ts +2 -4
  95. package/src/types/ClientState.ts +8 -0
  96. package/src/types/InternalState.ts +7 -0
  97. package/src/types/LayoutHandler.ts +2 -4
  98. package/src/types/LayoutPath.ts +10 -0
  99. package/src/types/ManifestRoute.ts +2 -4
  100. package/src/types/PageEndPoint.ts +5 -5
  101. package/src/types/PageLoadEvent.ts +2 -4
  102. package/src/types/PageServerAction.ts +1 -1
  103. package/src/types/PageServerEndPoint.ts +2 -4
  104. package/src/types/PageServerLoad.ts +1 -1
  105. package/src/types/PageState.ts +8 -0
  106. package/src/types/RouteHandler.ts +2 -4
  107. package/src/types/RouteLayoutHandler.ts +2 -4
  108. package/src/types/RouteMatchResult.ts +2 -4
  109. package/src/types/ServerHook.ts +3 -7
  110. package/src/types/ServerLoadEvent.ts +2 -4
  111. package/src/types/ServerRequest.ts +1 -1
  112. package/src/{site → utils}/pathToRegex.ts +1 -1
  113. package/{tsup.config.ts → tsdown.config.ts} +3 -4
  114. package/dist/Server-BC_dixZQ.d.cts +0 -71
  115. package/dist/Server-BC_dixZQ.d.ts +0 -71
  116. package/dist/Site-4tGiw2-I.d.ts +0 -63
  117. package/dist/Site-BXX-S541.d.cts +0 -63
  118. package/dist/bin/index.cjs +0 -755
  119. package/dist/bin/index.cjs.map +0 -1
  120. package/dist/bin/index.d.cts +0 -1
  121. package/dist/chunk-565UOT3F.js +0 -18
  122. package/dist/chunk-565UOT3F.js.map +0 -1
  123. package/dist/chunk-B2UNTYN5.js +0 -68
  124. package/dist/chunk-B2UNTYN5.js.map +0 -1
  125. package/dist/chunk-KKFND4HK.js +0 -85
  126. package/dist/chunk-KKFND4HK.js.map +0 -1
  127. package/dist/chunk-LO7P2LJE.js +0 -278
  128. package/dist/chunk-LO7P2LJE.js.map +0 -1
  129. package/dist/chunk-PTVHEJZ5.js +0 -152
  130. package/dist/chunk-PTVHEJZ5.js.map +0 -1
  131. package/dist/chunk-RHDSM5GS.js +0 -357
  132. package/dist/chunk-RHDSM5GS.js.map +0 -1
  133. package/dist/chunk-SJVHHGVW.js +0 -13
  134. package/dist/chunk-SJVHHGVW.js.map +0 -1
  135. package/dist/chunk-WVXAN4IV.js +0 -50
  136. package/dist/chunk-WVXAN4IV.js.map +0 -1
  137. package/dist/index.cjs +0 -322
  138. package/dist/index.cjs.map +0 -1
  139. package/dist/index.d.cts +0 -199
  140. package/dist/metafile-cjs.json +0 -1
  141. package/dist/metafile-esm.json +0 -1
  142. package/dist/response.cjs +0 -150
  143. package/dist/response.cjs.map +0 -1
  144. package/dist/response.d.cts +0 -166
  145. package/dist/response.js.map +0 -1
  146. package/dist/run.cjs +0 -764
  147. package/dist/run.cjs.map +0 -1
  148. package/dist/run.d.cts +0 -14
  149. package/dist/run.js.map +0 -1
  150. package/dist/server.cjs +0 -390
  151. package/dist/server.cjs.map +0 -1
  152. package/dist/server.d.cts +0 -17
  153. package/dist/server.js.map +0 -1
  154. package/dist/state.cjs +0 -44
  155. package/dist/state.cjs.map +0 -1
  156. package/dist/state.d.cts +0 -11
  157. package/dist/state.js.map +0 -1
  158. package/dist/test.cjs +0 -538
  159. package/dist/test.cjs.map +0 -1
  160. package/dist/test.d.cts +0 -15
  161. package/src/response/moved.ts +0 -16
  162. package/src/response/redirect.ts +0 -17
  163. package/src/site/prepareTemplate.ts +0 -20
  164. package/src/state/Page.ts +0 -8
@@ -1,38 +1,33 @@
1
1
  import manifest from "@torpor/build/manifest";
2
- // @ts-ignore This errors in the Cloudflare build?
3
- import { $page } from "@torpor/build/state";
4
- import { clearLayoutSlot, fillLayoutSlot, hydrate } from "@torpor/view";
5
- import { type Component, type SlotRender } from "@torpor/view";
6
- import { mount } from "@torpor/view";
7
- import type PageEndPoint from "../types/PageEndPoint.ts";
8
- import type PageServerEndPoint from "../types/PageServerEndPoint.ts";
9
- import type RouteHandler from "../types/RouteHandler.ts";
2
+ import loadData from "../nav/loadData.ts";
3
+ import navigate from "../nav/navigate.ts";
4
+ import client from "../state/client";
5
+ import type LayoutPath from "../types/LayoutPath";
6
+ import type PageEndPoint from "../types/PageEndPoint";
7
+ import type PageServerEndPoint from "../types/PageServerEndPoint";
10
8
  import Router from "./Router.ts";
11
9
 
12
10
  // Build the router from the Site object created by the user
13
11
  const router = new Router();
14
12
  router.addPages(manifest.routes);
15
13
 
16
- // Store the current layout stack, so that we can re-use parts of it as necessary
17
- interface LayoutPath {
18
- path: string;
19
- /** Whether to re-use this layout */
20
- reuse: boolean;
21
- /** Data loaded from endpoints that should be re-used */
22
- data: any;
23
- /** The range of the UI in this layout's slot */
24
- slotRange: any;
25
- }
26
- let layoutStack: LayoutPath[] = [];
27
-
28
- // TODO: Probably expire after 30 seconds or something?
29
- let prefetchedData: Record<string, any> = {};
14
+ // Put it in client state so that it can be accessed from navigate. We can't
15
+ // just import it to navigate because that gets built by tsdown, without Vite
16
+ // and the manifest
17
+ client.router = router;
30
18
 
31
19
  // Intercept clicks on links
32
20
  window.addEventListener("click", async (e) => {
33
21
  if (e.target && (e.target as HTMLElement).tagName === "A") {
22
+ const link = e.target as HTMLAnchorElement;
23
+
24
+ if (ignoreAnchorMouseEvent(link, e)) {
25
+ return;
26
+ }
27
+
34
28
  e.preventDefault();
35
- const href = (e.target as HTMLLinkElement).href;
29
+
30
+ const href = link.href;
36
31
  const url = new URL(href);
37
32
 
38
33
  if (await navigate(url)) {
@@ -46,12 +41,17 @@ window.addEventListener("click", async (e) => {
46
41
  // Intercept mouseover/focus/touchstart on links for prefetching
47
42
  // TODO: settings e.g. <a href="" data-tb-prefetch="eager"/"visible"/"hover"/"touch">
48
43
  window.addEventListener("mouseover", maybePrefetch);
49
- window.addEventListener("focus", maybePrefetch);
50
44
  window.addEventListener("touchstart", maybePrefetch);
51
45
 
52
- async function maybePrefetch(e: Event) {
46
+ async function maybePrefetch(e: MouseEvent | TouchEvent) {
53
47
  if (e.target && (e.target as HTMLElement).tagName === "A") {
54
- const href = (e.target as HTMLLinkElement).href;
48
+ const link = e.target as HTMLAnchorElement;
49
+
50
+ if (ignoreAnchorMouseEvent(link, e)) {
51
+ return;
52
+ }
53
+
54
+ const href = link.href;
55
55
  const url = new URL(href);
56
56
 
57
57
  const path = url.pathname;
@@ -76,6 +76,19 @@ async function maybePrefetch(e: Event) {
76
76
  }
77
77
  }
78
78
 
79
+ function ignoreAnchorMouseEvent(link: HTMLAnchorElement, e: MouseEvent | TouchEvent) {
80
+ // Ignore the event if a modifier key is pressed, or target is not
81
+ // "_self", or if rel="external"
82
+ return (
83
+ e.altKey ||
84
+ e.ctrlKey ||
85
+ e.metaKey ||
86
+ e.shiftKey ||
87
+ (link.target && link.target !== "_self") ||
88
+ link.rel === "external"
89
+ );
90
+ }
91
+
79
92
  // Listen for changes to the URL that occur when the user navigates using the
80
93
  // back or forward buttons
81
94
  window.addEventListener("popstate", async () => {
@@ -83,254 +96,8 @@ window.addEventListener("popstate", async () => {
83
96
  });
84
97
 
85
98
  // Do the initial navigation and hydration
86
- navigateToLocation(document.location, true);
99
+ await navigateToLocation(document.location, true);
87
100
 
88
101
  async function navigateToLocation(location: Location, firstTime = false) {
89
102
  return await navigate(new URL(location.toString()), firstTime);
90
103
  }
91
-
92
- async function navigate(url: URL, firstTime = false): Promise<boolean> {
93
- let parent = document.getElementById("app");
94
- if (!parent) {
95
- // TODO: 500
96
- console.log("500");
97
- return false;
98
- }
99
-
100
- const path = url.pathname;
101
- const query = url.searchParams;
102
-
103
- console.log(`navigating to '${path}'${query.size ? ` with ${query}` : ""}`);
104
-
105
- const route = router.match(path, query);
106
- if (!route) {
107
- // TODO: 404
108
- console.log("404");
109
- return false;
110
- }
111
-
112
- // Update $page before building the components
113
- $page.url = url;
114
- if (path.endsWith("/_error")) {
115
- $page.status = parseInt(query.get("status") ?? "404");
116
- $page.error.message = query.get("message") ?? "";
117
- // Make it look a bit classier by removing the query
118
- window.history.replaceState({}, "", url.toString().split("?")[0]);
119
- } else {
120
- $page.status = 200;
121
- }
122
-
123
- const handler = route.handler;
124
- const params = route.params || {};
125
-
126
- // There must be a client endpoint with a component
127
- const clientEndPoint: PageEndPoint | undefined = (await handler.endPoint()).default;
128
- if (!clientEndPoint?.component) {
129
- // TODO: 404
130
- console.log("404");
131
- return false;
132
- }
133
-
134
- // There may be a server endpoint
135
- const serverEndPoint: PageServerEndPoint | undefined =
136
- handler.serverEndPoint && (await handler.serverEndPoint())?.default;
137
-
138
- let newLayoutStack: LayoutPath[] = [];
139
-
140
- // Pass the data into $props
141
- // TODO: Don't load if this is the first time -- it should have been passed to us, somehow...
142
- const data = await loadData(
143
- handler,
144
- params,
145
- path,
146
- newLayoutStack,
147
- clientEndPoint,
148
- serverEndPoint,
149
- );
150
- const $props: Record<string, any> = { data };
151
-
152
- layoutStack.push({ path: route.handler.path, data: {}, reuse: false, slotRange: null });
153
- layoutStack = newLayoutStack;
154
-
155
- // If there are layouts, work our way upwards, pushing each component into
156
- // the default slot of its parent
157
- // TODO: There's probably a nicer way to do this with reducers or something
158
- let component = clientEndPoint.component as Component;
159
- let slots: Record<string, SlotRender> | undefined = undefined;
160
- if (handler.layouts) {
161
- let slotFunctions: SlotRender[] = [];
162
- // The last slot function will render the client component
163
- slotFunctions[handler.layouts.length] = function clientComponent(parent, anchor, _, $context) {
164
- let i = layoutStack.length - 1;
165
- layoutStack[i].slotRange = fillLayoutSlot(
166
- clientEndPoint.component!,
167
- slotFunctions[i + 1],
168
- parent,
169
- anchor,
170
- $props,
171
- $context,
172
- );
173
- };
174
-
175
- // Each earlier slot function will render a layout
176
- for (let i = handler.layouts.length - 1; i >= 0; i--) {
177
- const layoutEndPoint: PageEndPoint | undefined = (await handler.layouts[i].endPoint())
178
- ?.default;
179
- if (layoutEndPoint?.component) {
180
- if (layoutStack[i].reuse) {
181
- // Set the parent to add the new content to (from the old
182
- // content), clear the range under this point, and set the
183
- // component to the slot function within this layout
184
- parent = layoutStack[i].slotRange.startNode.parentNode as HTMLElement;
185
- clearLayoutSlot(layoutStack[i].slotRange);
186
- component = slotFunctions[i + 1];
187
- break;
188
- } else if (i === 0) {
189
- component = layoutEndPoint.component as Component;
190
- slots = { _: slotFunctions[i + 1] };
191
- } else {
192
- slotFunctions[i] = function layoutComponent(parent, anchor, _, $context) {
193
- layoutStack[i - 1].slotRange = fillLayoutSlot(
194
- layoutEndPoint.component!,
195
- slotFunctions[i + 1],
196
- parent,
197
- anchor,
198
- $props,
199
- $context,
200
- );
201
- };
202
- }
203
- }
204
- }
205
- }
206
-
207
- if (firstTime) {
208
- hydrate(parent, component, $props, slots);
209
- } else {
210
- try {
211
- mount(parent, component, $props, slots);
212
- } catch (error) {
213
- // TODO: Show a proper Error component
214
- parent.innerHTML = '<span style="color: red">Script syntax error</span><p>' + error + "</p>";
215
- console.log(error);
216
- }
217
- }
218
-
219
- // Reset prefetched data on each navigation
220
- prefetchedData = {};
221
-
222
- return true;
223
- }
224
-
225
- async function loadData(
226
- handler: RouteHandler,
227
- params: Record<string, string>,
228
- path: string,
229
- newLayoutStack: LayoutPath[],
230
- clientEndPoint: PageEndPoint | undefined,
231
- serverEndPoint: PageServerEndPoint | undefined,
232
- prefetch = false,
233
- ) {
234
- let data = {};
235
- if (handler.layouts) {
236
- for (let [i, layout] of handler.layouts.entries()) {
237
- let layoutPath = layout.path;
238
- for (let key in params) {
239
- layoutPath = layoutPath.replace(`[${key}]`, params[key]);
240
- }
241
- if (layoutStack.at(i)?.path === layoutPath) {
242
- // We've already loaded this layout, we can just re-use its data and UI
243
- if (!prefetch) {
244
- layoutStack[i].reuse = true;
245
- }
246
- newLayoutStack[i] = layoutStack[i];
247
- Object.assign(data, layoutStack[i].data);
248
- } else {
249
- const stackLayout = { path: layoutPath, data: {}, reuse: false, slotRange: null };
250
- const layoutEndPoint: PageEndPoint | undefined = (await layout.endPoint())?.default;
251
- const layoutServerEndPoint: PageServerEndPoint | undefined =
252
- layout.serverEndPoint && (await layout.serverEndPoint())?.default;
253
- const layoutResponse = await loadClientAndServerData(
254
- stackLayout.data,
255
- document.location.origin + layoutPath,
256
- params,
257
- layoutEndPoint,
258
- layoutServerEndPoint,
259
- );
260
- if (layoutResponse?.ok === false) {
261
- return false;
262
- }
263
- Object.assign(data, stackLayout.data);
264
- newLayoutStack.push(stackLayout);
265
- }
266
- }
267
- }
268
- let endPointResponse = await loadClientAndServerData(
269
- data,
270
- document.location.origin + path,
271
- params,
272
- clientEndPoint,
273
- serverEndPoint,
274
- );
275
- if (endPointResponse?.ok === false) {
276
- return false;
277
- }
278
- return data;
279
- }
280
-
281
- async function loadClientAndServerData(
282
- data: Record<string, any>,
283
- location: string,
284
- params: Record<string, string>,
285
- clientEndPoint?: PageEndPoint,
286
- serverEndPoint?: PageServerEndPoint,
287
- ): Promise<Response | void> {
288
- if (clientEndPoint?.load) {
289
- if (prefetchedData[location]) {
290
- Object.assign(data, prefetchedData[location]);
291
- } else {
292
- const clientUrl = new URL(document.location.href);
293
- const clientParams = buildClientParams(clientUrl, params, data);
294
- const clientResponse = await clientEndPoint.load(clientParams);
295
- if (clientResponse) {
296
- if (clientResponse.ok) {
297
- if (clientResponse.headers.get("Content-Type")?.includes("application/json")) {
298
- const clientData = await clientResponse.json();
299
- Object.assign(data, clientData);
300
- prefetchedData[location] = clientData;
301
- }
302
- } else {
303
- return clientResponse;
304
- }
305
- }
306
- }
307
- }
308
- if (serverEndPoint?.load) {
309
- const serverUrl = location.replace(/\/$/, "") + "/~server";
310
-
311
- if (prefetchedData[serverUrl]) {
312
- Object.assign(data, prefetchedData[serverUrl]);
313
- } else {
314
- const serverResponse = await fetch(serverUrl);
315
- if (serverResponse) {
316
- if (serverResponse.ok) {
317
- if (serverResponse.headers.get("Content-Type")?.includes("application/json")) {
318
- const serverData = await serverResponse.json();
319
- Object.assign(data, serverData);
320
- prefetchedData[serverUrl] = serverData;
321
- }
322
- } else {
323
- return serverResponse;
324
- }
325
- }
326
- }
327
- }
328
- }
329
-
330
- function buildClientParams(url: URL, params: Record<string, string>, data: Record<string, any>) {
331
- return {
332
- url,
333
- params,
334
- data,
335
- };
336
- }
@@ -1,4 +1,4 @@
1
- import Adapter from "../types/Adapter";
1
+ import type Adapter from "../types/Adapter";
2
2
 
3
3
  // TODO: This should be an adapter-auto package that conditionally loads other packages...
4
4
 
@@ -1,14 +1,13 @@
1
1
  import manifest from "@torpor/build/manifest";
2
- // @ts-ignore This errors in the Cloudflare build?
3
- import { $page } from "@torpor/build/state";
4
2
  import { type ServerComponent, type ServerSlotRender } from "@torpor/view";
5
3
  import notFound from "../response/notFound.ts";
6
4
  import ok from "../response/ok.ts";
7
5
  import seeOther from "../response/seeOther.ts";
8
6
  import ServerEvent from "../server/ServerEvent.ts";
7
+ import $page from "../state/$page";
9
8
  import type PageEndPoint from "../types/PageEndPoint.ts";
10
9
  import type PageServerEndPoint from "../types/PageServerEndPoint.ts";
11
- import RouteHandler from "../types/RouteHandler.ts";
10
+ import type RouteHandler from "../types/RouteHandler.ts";
12
11
  import {
13
12
  ERROR_ROUTE,
14
13
  HOOK_ROUTE,
@@ -30,33 +29,6 @@ router.addPages(manifest.routes);
30
29
 
31
30
  //console.log(`routes:\n ${router.routes.map((r) => r.path).join("\n ")}`);
32
31
 
33
- async function handleResponse(response: Response): Promise<Response> {
34
- $page.status = response.status;
35
-
36
- // Success codes and redirect codes are acceptable
37
- if (response.status >= 200 && response.status <= 399) {
38
- return response;
39
- }
40
-
41
- // It's an error, so redirect to the error page
42
- // We're just pushing the status and message in the URL, but maybe there's a
43
- // more sophisticated way to do this?
44
- // TODO: Should be returning loadView rather than redirecting
45
- // TODO: Should be returning the NEAREST error page to this path, including layouts
46
- let params = new URLSearchParams();
47
- params.append("status", $page.status.toString());
48
- let message = await response.text();
49
- if (response.headers.get("Content-Type")?.includes("application/json")) {
50
- const data = JSON.parse(message);
51
- message = data.message ?? message;
52
- }
53
- if (message) {
54
- $page.error.message = message;
55
- params.append("message", message);
56
- }
57
- return seeOther(`/_error?${params.toString()}`);
58
- }
59
-
60
32
  export async function load(ev: ServerEvent, template: string): Promise<Response> {
61
33
  //const url = new URL(`http://${process.env.HOST ?? "localhost"}${ev.request.url}`);
62
34
  const url = new URL(ev.request.url);
@@ -69,19 +41,18 @@ export async function load(ev: ServerEvent, template: string): Promise<Response>
69
41
  if (!route) {
70
42
  return handleResponse(notFound());
71
43
  }
44
+ const handler = route.handler;
45
+ const params = route.params || {};
72
46
 
73
47
  // Update $page before building the components
74
48
  $page.url = url;
75
49
  if (path.endsWith("/_error")) {
76
50
  $page.status = parseInt(query.get("status") ?? "404");
77
- $page.error.message = query.get("message") ?? "";
51
+ $page.error = { message: query.get("message") ?? "" };
78
52
  } else {
79
53
  $page.status = 200;
80
54
  }
81
55
 
82
- const handler = route.handler;
83
- const params = route.params || {};
84
-
85
56
  // TODO: Hit the server hook out here
86
57
  // We could maybe set data loading up as middleware on a route??????
87
58
 
@@ -92,7 +63,7 @@ export async function load(ev: ServerEvent, template: string): Promise<Response>
92
63
  if (ev.request.method === "GET") {
93
64
  return handleResponse(await loadView(ev, url, handler, params, template));
94
65
  } else if (ev.request.method === "POST") {
95
- return handleResponse(await runAction(ev, url, handler, params, query));
66
+ return handleResponse(await runAction(ev, url, handler, params, query, template), true);
96
67
  }
97
68
  break;
98
69
  }
@@ -121,6 +92,42 @@ export async function load(ev: ServerEvent, template: string): Promise<Response>
121
92
  return notFound();
122
93
  }
123
94
 
95
+ async function handleResponse(response: Response, fromForm = false): Promise<Response> {
96
+ $page.status = response.status;
97
+
98
+ // Success codes and redirect codes are acceptable
99
+ if (response.status >= 200 && response.status <= 399) {
100
+ return response;
101
+ }
102
+
103
+ // 4xx error codes are acceptable from form actions
104
+ if (fromForm && response.status >= 400 && response.status <= 499) {
105
+ return response;
106
+ }
107
+
108
+ // It's an error, so redirect to the error page
109
+ // We're just pushing the status and message in the URL, but maybe there's a
110
+ // more sophisticated way to do this?
111
+ // TODO: Should be returning loadView rather than redirecting
112
+ // TODO: Should be returning the NEAREST error page to this path, including layouts
113
+ let params = new URLSearchParams();
114
+ params.append("status", $page.status.toString());
115
+ let message = await response.text();
116
+ if (response.headers.get("Content-Type")?.includes("application/json")) {
117
+ const data = JSON.parse(message);
118
+ message = data.message ?? message;
119
+ }
120
+ if (message) {
121
+ if ($page.error) {
122
+ $page.error.message = message;
123
+ } else {
124
+ $page.error = { message };
125
+ }
126
+ params.append("message", message);
127
+ }
128
+ return seeOther(`/_error?${params.toString()}`);
129
+ }
130
+
124
131
  async function loadData(
125
132
  ev: ServerEvent,
126
133
  url: URL,
@@ -155,6 +162,8 @@ async function loadView(
155
162
  handler: RouteHandler,
156
163
  params: Record<string, string>,
157
164
  template: string,
165
+ formStatus?: number,
166
+ form?: Record<string, string>,
158
167
  ) {
159
168
  // There must be a client endpoint with a component
160
169
  const clientEndPoint: PageEndPoint | undefined = (await handler.endPoint()).default;
@@ -209,19 +218,23 @@ async function loadView(
209
218
  if (endPointResponse?.ok === false) {
210
219
  return endPointResponse;
211
220
  }
212
- let $props: Record<string, any> = { data };
221
+ let $props: Record<string, any> = { data, form };
222
+
223
+ let styles = "";
213
224
 
214
225
  // If there are layouts, work our way upwards, pushing each component into
215
226
  // the default slot of its parent
216
227
  // TODO: Also handle layout server data
217
228
  // TODO: There's probably a nicer way to do this with reducers or something
218
- // TODO: Re-use parent layouts
219
229
  let component = clientEndPoint.component as ServerComponent;
220
230
  let slots: Record<string, ServerSlotRender> | undefined = undefined;
221
231
  if (handler.layouts) {
222
232
  let slotFunctions: ServerSlotRender[] = [];
223
- slotFunctions[handler.layouts.length] = (_, $context) =>
224
- (clientEndPoint.component as ServerComponent)($props, $context);
233
+ slotFunctions[handler.layouts.length] = (_, $context) => {
234
+ let { body, head } = (clientEndPoint.component as ServerComponent)($props, $context);
235
+ styles += head;
236
+ return body;
237
+ };
225
238
  for (let i = handler.layouts.length - 1; i >= 0; i--) {
226
239
  const layoutEndPoint: PageEndPoint | undefined = (await handler.layouts[i].endPoint())
227
240
  ?.default;
@@ -230,10 +243,13 @@ async function loadView(
230
243
  component = layoutEndPoint.component as ServerComponent;
231
244
  slots = { _: slotFunctions[i + 1] };
232
245
  } else {
233
- slotFunctions[i] = (_, $context) =>
234
- (layoutEndPoint.component as ServerComponent)($props, $context, {
246
+ slotFunctions[i] = (_, $context) => {
247
+ let { body, head } = (layoutEndPoint.component as ServerComponent)($props, $context, {
235
248
  _: slotFunctions[i + 1],
236
249
  });
250
+ styles += head;
251
+ return body;
252
+ };
237
253
  }
238
254
  }
239
255
  }
@@ -241,8 +257,15 @@ async function loadView(
241
257
 
242
258
  let html;
243
259
  try {
244
- html = component($props, undefined, slots);
245
- html = template.replace("%COMPONENT_HTML%", html);
260
+ let { body, head } = component($props, undefined, slots);
261
+
262
+ // Put the form info in a hidden input so that it can be accessed on the client
263
+ if (form) {
264
+ body += `\n<input type="hidden" id="t-form-data" value='${JSON.stringify(form).replaceAll("'", "\\'")}' />`;
265
+ }
266
+
267
+ styles += head;
268
+ html = template.replace("%COMPONENT_BODY%", body).replace("%COMPONENT_HEAD%", styles);
246
269
  } catch (error) {
247
270
  // TODO: Show a proper Error component
248
271
  html = '<span style="color: red">Script syntax error</span><p>' + error + "</p>";
@@ -250,7 +273,7 @@ async function loadView(
250
273
  }
251
274
 
252
275
  return new Response(html, {
253
- status: 200,
276
+ status: formStatus ?? 200,
254
277
  headers: {
255
278
  "Content-Type": "text/html",
256
279
  },
@@ -263,6 +286,7 @@ async function runAction(
263
286
  handler: RouteHandler,
264
287
  params: Record<string, string>,
265
288
  query: URLSearchParams,
289
+ template: string,
266
290
  ) {
267
291
  const actionName = (Array.from(query.keys())[0] || "default").replace(/^\//, "");
268
292
  const serverEndPoint: PageServerEndPoint | undefined =
@@ -270,7 +294,6 @@ async function runAction(
270
294
  if (serverEndPoint?.actions) {
271
295
  const action = serverEndPoint.actions[actionName];
272
296
  if (action) {
273
- // TODO: form.errors etc
274
297
  const serverParams = buildServerParams(ev, url, params);
275
298
 
276
299
  if (handler.serverHook) {
@@ -280,11 +303,24 @@ async function runAction(
280
303
  }
281
304
  }
282
305
 
283
- const result = await action(serverParams); // <--
284
-
285
- // If there was no response returned from the action (such as errors
286
- // or a redirect), reload the page by redirecting
287
- return result || seeOther(url.pathname);
306
+ const result = await action(serverParams);
307
+
308
+ // If no result, ok or 4xx error, re-render the view with the form result
309
+ // If redirect or server error, just return the result to handle it as normal
310
+ // TODO: do not re-run hooks?
311
+ if (
312
+ !result ||
313
+ (result.status >= 200 && result.status <= 299) ||
314
+ (result.status >= 400 && result.status <= 499)
315
+ ) {
316
+ const newUrl = new URL(url.pathname, url);
317
+ const formStatus = result?.status;
318
+ const form = await result?.json();
319
+ $page.form = form;
320
+ return await loadView(ev, newUrl, handler, params, template, formStatus, form);
321
+ } else {
322
+ return result;
323
+ }
288
324
  }
289
325
  }
290
326
 
@@ -1,15 +1,6 @@
1
- import { $watch } from "@torpor/view";
2
- import Page from "./Page";
1
+ import type PageState from "../types/PageState";
2
+ import internal from "./internal";
3
3
 
4
- const $page: Page = $watch(
5
- {
6
- status: 404,
7
- url: new URL("http://localhost"),
8
- error: {
9
- message: "",
10
- },
11
- },
12
- { shallow: true },
13
- );
4
+ const $page: PageState = internal().$page;
14
5
 
15
6
  export default $page;
@@ -0,0 +1,6 @@
1
+ import type ClientState from "../types/ClientState";
2
+ import internal from "./internal";
3
+
4
+ const client: ClientState = internal().client;
5
+
6
+ export default client;
@@ -0,0 +1,45 @@
1
+ import { $watch } from "@torpor/view";
2
+ import type ClientState from "../types/ClientState";
3
+ import type InternalState from "../types/InternalState";
4
+ import type PageState from "../types/PageState";
5
+
6
+ // Do a weird little dance to ensure that everyone has the same $page and
7
+ // client, regardless of if they are entering via serverEntry/clientEntry or the
8
+ // built library. There may be an easier way to do this!
9
+
10
+ const INTERNAL_GLOBAL: unique symbol = Symbol.for("torpor-internal");
11
+
12
+ export default function internal(): InternalState {
13
+ // @ts-ignore
14
+ if (globalThis[INTERNAL_GLOBAL] === undefined) {
15
+ // TODO: Should use the SSR $watch if entering via serverEntry
16
+ const $page: PageState = $watch(
17
+ {
18
+ status: 404,
19
+ url: new URL("http://localhost"),
20
+ form: {},
21
+ error: {
22
+ message: "",
23
+ },
24
+ },
25
+ {
26
+ shallow: true,
27
+ },
28
+ );
29
+ const client: ClientState = {
30
+ // @ts-ignore This will definitely, 100% get set
31
+ router: null,
32
+ layoutStack: [],
33
+ // TODO: Probably expire after 30 seconds or something?
34
+ prefetchedData: {},
35
+ };
36
+
37
+ // @ts-ignore
38
+ globalThis[INTERNAL_GLOBAL] = {
39
+ $page,
40
+ client,
41
+ } satisfies InternalState;
42
+ }
43
+ // @ts-ignore
44
+ return globalThis[INTERNAL_GLOBAL] as InternalState;
45
+ }