@torpor/build 0.1.31 → 0.1.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # @torpor/build
2
2
 
3
+ ## 0.1.32
4
+
5
+ ### Patch Changes
6
+
7
+ - 951a86f: Feat: re-use layout data and UI
8
+ - 182d436: Feat: prefetching link data
9
+ - Updated dependencies [1866f30]
10
+ - Updated dependencies [a31647f]
11
+ - Updated dependencies [951a86f]
12
+ - Updated dependencies [6d6fb89]
13
+ - Updated dependencies [9c13f0f]
14
+ - @torpor/view@0.1.6
15
+
3
16
  ## 0.1.31
4
17
 
5
18
  ### Patch Changes
package/dist/index.d.cts CHANGED
@@ -76,7 +76,7 @@ type PageEndPoint = {
76
76
  /**
77
77
  * Loads data for the page.
78
78
  */
79
- load?: (event: PageLoadEvent) => any;
79
+ load?: (event: PageLoadEvent) => Response | Promise<Response | undefined> | Promise<void> | void;
80
80
  /**
81
81
  * The component that is displayed for the page.
82
82
  */
package/dist/index.d.ts CHANGED
@@ -76,7 +76,7 @@ type PageEndPoint = {
76
76
  /**
77
77
  * Loads data for the page.
78
78
  */
79
- load?: (event: PageLoadEvent) => any;
79
+ load?: (event: PageLoadEvent) => Response | Promise<Response | undefined> | Promise<void> | void;
80
80
  /**
81
81
  * The component that is displayed for the page.
82
82
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torpor/build",
3
- "version": "0.1.31",
3
+ "version": "0.1.32",
4
4
  "description": "A framework for building sites and apps with Torpor",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -53,17 +53,17 @@
53
53
  "dependencies": {
54
54
  "cookie": "^1.0.2",
55
55
  "dotenv": "^16.5.0",
56
- "vite": "^6.3.5",
56
+ "vite": "^7.0.0",
57
57
  "vite-tsconfig-paths": "^5.1.4",
58
- "@torpor/unplugin": "^0.1.16",
59
- "@torpor/view": "^0.1.5"
58
+ "@torpor/view": "^0.1.6",
59
+ "@torpor/unplugin": "^0.1.16"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@trivago/prettier-plugin-sort-imports": "^5.2.2",
63
63
  "tsup": "^8.5.0",
64
64
  "tsx": "^4.20.3",
65
65
  "typescript": "^5.8.3",
66
- "vitest": "^3.2.3"
66
+ "vitest": "^3.2.4"
67
67
  },
68
68
  "scripts": {
69
69
  "build": "tsc --noEmit && tsup",
@@ -1,16 +1,33 @@
1
1
  import manifest from "@torpor/build/manifest";
2
2
  // @ts-ignore This errors in the Cloudflare build?
3
3
  import { $page } from "@torpor/build/state";
4
- import { hydrate, mount } from "@torpor/view";
4
+ import { clearLayoutSlot, hydrate, runLayoutSlot } from "@torpor/view";
5
5
  import { type Component, type SlotRender } from "@torpor/view";
6
+ import { mount } from "@torpor/view";
6
7
  import type PageEndPoint from "../types/PageEndPoint.ts";
7
8
  import type PageServerEndPoint from "../types/PageServerEndPoint.ts";
9
+ import type RouteHandler from "../types/RouteHandler.ts";
8
10
  import Router from "./Router.ts";
9
11
 
10
12
  // Build the router from the Site object created by the user
11
13
  const router = new Router();
12
14
  router.addPages(manifest.routes);
13
15
 
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> = {};
30
+
14
31
  // Intercept clicks on links
15
32
  window.addEventListener("click", async (e) => {
16
33
  if (e.target && (e.target as HTMLElement).tagName === "A") {
@@ -26,6 +43,39 @@ window.addEventListener("click", async (e) => {
26
43
  }
27
44
  });
28
45
 
46
+ // Intercept mouseover/focus/touchstart on links for prefetching
47
+ // TODO: settings e.g. <a href="" data-tb-prefetch="eager"/"visible"/"hover"/"touch">
48
+ window.addEventListener("mouseover", maybePrefetch);
49
+ window.addEventListener("focus", maybePrefetch);
50
+ window.addEventListener("touchstart", maybePrefetch);
51
+
52
+ async function maybePrefetch(e: Event) {
53
+ if (e.target && (e.target as HTMLElement).tagName === "A") {
54
+ const href = (e.target as HTMLLinkElement).href;
55
+ const url = new URL(href);
56
+
57
+ const path = url.pathname;
58
+ const query = url.searchParams;
59
+ const route = router.match(path, query);
60
+ if (route) {
61
+ const handler = route.handler;
62
+ const params = route.params || {};
63
+
64
+ // There should be a client endpoint
65
+ const clientEndPoint: PageEndPoint | undefined = (await handler.endPoint()).default;
66
+
67
+ // There may be a server endpoint
68
+ const serverEndPoint: PageServerEndPoint | undefined =
69
+ handler.serverEndPoint && (await handler.serverEndPoint())?.default;
70
+
71
+ // Just dummy this up
72
+ let newStack: LayoutPath[] = [];
73
+
74
+ await loadData(handler, params, path, newStack, clientEndPoint, serverEndPoint, true);
75
+ }
76
+ }
77
+ }
78
+
29
79
  // Listen for changes to the URL that occur when the user navigates using the
30
80
  // back or forward buttons
31
81
  window.addEventListener("popstate", async () => {
@@ -40,7 +90,7 @@ async function navigateToLocation(location: Location, firstTime = false) {
40
90
  }
41
91
 
42
92
  async function navigate(url: URL, firstTime = false): Promise<boolean> {
43
- const parent = document.getElementById("app");
93
+ let parent = document.getElementById("app");
44
94
  if (!parent) {
45
95
  // TODO: 500
46
96
  console.log("500");
@@ -85,78 +135,83 @@ async function navigate(url: URL, firstTime = false): Promise<boolean> {
85
135
  const serverEndPoint: PageServerEndPoint | undefined =
86
136
  handler.serverEndPoint && (await handler.serverEndPoint())?.default;
87
137
 
138
+ let newLayoutStack: LayoutPath[] = [];
139
+
88
140
  // Pass the data into $props
89
141
  // TODO: Don't load if this is the first time -- it should have been passed to us, somehow...
90
- let data = {};
91
- if (handler.layouts) {
92
- for (let layout of handler.layouts) {
93
- let layoutPath = layout.path;
94
- for (let key in params) {
95
- layoutPath = layoutPath.replace(`[${key}]`, params[key]);
96
- }
97
- const layoutEndPoint: PageEndPoint | undefined = (await layout.endPoint())?.default;
98
- const layoutServerEndPoint: PageServerEndPoint | undefined =
99
- layout.serverEndPoint && (await layout.serverEndPoint())?.default;
100
- const layoutResponse = await loadClientAndServerData(
101
- data,
102
- document.location.origin + layoutPath,
103
- params,
104
- layoutEndPoint,
105
- layoutServerEndPoint,
106
- );
107
- if (layoutResponse?.ok === false) {
108
- return false;
109
- }
110
- }
111
- }
112
- let endPointResponse = await loadClientAndServerData(
113
- data,
114
- document.location.origin + path,
142
+ const data = await loadData(
143
+ handler,
115
144
  params,
145
+ path,
146
+ newLayoutStack,
116
147
  clientEndPoint,
117
148
  serverEndPoint,
118
149
  );
119
- if (endPointResponse?.ok === false) {
120
- return false;
121
- }
122
- let $props: Record<string, any> = { data };
150
+ const $props: Record<string, any> = { data };
151
+
152
+ layoutStack.push({ path: route.handler.path, data: {}, reuse: false, slotRange: null });
153
+ layoutStack = newLayoutStack;
123
154
 
124
155
  // If there are layouts, work our way upwards, pushing each component into
125
156
  // the default slot of its parent
126
- // TODO: Also handle layout server data
127
157
  // TODO: There's probably a nicer way to do this with reducers or something
128
158
  let component = clientEndPoint.component as Component;
129
159
  let slots: Record<string, SlotRender> | undefined = undefined;
130
160
  if (handler.layouts) {
131
161
  let slotFunctions: SlotRender[] = [];
132
- slotFunctions[handler.layouts.length] = (parent, anchor, _, context) =>
133
- clientEndPoint.component!(parent, anchor, $props, context);
162
+ // The last slot function will render the client component
163
+ slotFunctions[handler.layouts.length] = function clientComponent(parent, anchor, _, $context) {
164
+ if (clientEndPoint.component) {
165
+ let i = layoutStack.length - 1;
166
+ layoutStack[i].slotRange = runLayoutSlot(
167
+ clientEndPoint.component,
168
+ slotFunctions[i + 1],
169
+ parent,
170
+ anchor,
171
+ $props,
172
+ $context,
173
+ );
174
+ }
175
+ };
176
+
177
+ // Each earlier slot function will render a layout
134
178
  for (let i = handler.layouts.length - 1; i >= 0; i--) {
135
179
  const layoutEndPoint: PageEndPoint | undefined = (await handler.layouts[i].endPoint())
136
180
  ?.default;
137
181
  if (layoutEndPoint?.component) {
138
- if (i === 0) {
182
+ if (layoutStack[i].reuse) {
183
+ // Set the parent to add the new content to (from the old
184
+ // content), clear the range under this point, and set the
185
+ // component to the slot function within this layout
186
+ parent = layoutStack[i].slotRange.startNode.parentNode as HTMLElement;
187
+ clearLayoutSlot(layoutStack[i].slotRange);
188
+ component = slotFunctions[i + 1];
189
+ break;
190
+ } else if (i === 0) {
139
191
  component = layoutEndPoint.component as Component;
140
192
  slots = { _: slotFunctions[i + 1] };
141
193
  } else {
142
- slotFunctions[i] = (parent, anchor, _, context) =>
143
- layoutEndPoint.component!(parent, anchor, $props, context, {
144
- _: slotFunctions[i + 1],
145
- });
194
+ slotFunctions[i] = function layoutComponent(parent, anchor, _, $context) {
195
+ if (layoutEndPoint.component) {
196
+ layoutStack[i].slotRange = runLayoutSlot(
197
+ layoutEndPoint.component,
198
+ slotFunctions[i + 1],
199
+ parent,
200
+ anchor,
201
+ $props,
202
+ $context,
203
+ );
204
+ }
205
+ };
146
206
  }
147
207
  }
148
208
  }
149
209
  }
150
210
 
151
211
  if (firstTime) {
152
- console.log("hydrating");
153
212
  hydrate(parent, component, $props, slots);
154
213
  } else {
155
- // TODO: Clear ranges, reuse layouts etc
156
- parent.textContent = "";
157
-
158
214
  try {
159
- console.log("mounting");
160
215
  mount(parent, component, $props, slots);
161
216
  } catch (error) {
162
217
  // TODO: Show a proper Error component
@@ -165,9 +220,68 @@ async function navigate(url: URL, firstTime = false): Promise<boolean> {
165
220
  }
166
221
  }
167
222
 
223
+ // Reset prefetched data on each navigation
224
+ prefetchedData = {};
225
+
168
226
  return true;
169
227
  }
170
228
 
229
+ async function loadData(
230
+ handler: RouteHandler,
231
+ params: Record<string, string>,
232
+ path: string,
233
+ newLayoutStack: LayoutPath[],
234
+ clientEndPoint: PageEndPoint | undefined,
235
+ serverEndPoint: PageServerEndPoint | undefined,
236
+ prefetch = false,
237
+ ) {
238
+ let data = {};
239
+ if (handler.layouts) {
240
+ for (let [i, layout] of handler.layouts.entries()) {
241
+ let layoutPath = layout.path;
242
+ if (layoutStack.at(i)?.path === layoutPath) {
243
+ // We've already loaded this layout, we can just re-use its data and UI
244
+ if (!prefetch) {
245
+ layoutStack[i].reuse = true;
246
+ }
247
+ newLayoutStack[i] = layoutStack[i];
248
+ Object.assign(data, layoutStack[i].data);
249
+ } else {
250
+ const stackLayout = { path: layoutPath, data: {}, reuse: false, slotRange: null };
251
+ for (let key in params) {
252
+ layoutPath = layoutPath.replace(`[${key}]`, params[key]);
253
+ }
254
+ const layoutEndPoint: PageEndPoint | undefined = (await layout.endPoint())?.default;
255
+ const layoutServerEndPoint: PageServerEndPoint | undefined =
256
+ layout.serverEndPoint && (await layout.serverEndPoint())?.default;
257
+ const layoutResponse = await loadClientAndServerData(
258
+ stackLayout.data,
259
+ document.location.origin + layoutPath,
260
+ params,
261
+ layoutEndPoint,
262
+ layoutServerEndPoint,
263
+ );
264
+ if (layoutResponse?.ok === false) {
265
+ return false;
266
+ }
267
+ Object.assign(data, stackLayout.data);
268
+ newLayoutStack.push(stackLayout);
269
+ }
270
+ }
271
+ }
272
+ let endPointResponse = await loadClientAndServerData(
273
+ data,
274
+ document.location.origin + path,
275
+ params,
276
+ clientEndPoint,
277
+ serverEndPoint,
278
+ );
279
+ if (endPointResponse?.ok === false) {
280
+ return false;
281
+ }
282
+ return data;
283
+ }
284
+
171
285
  async function loadClientAndServerData(
172
286
  data: Record<string, any>,
173
287
  location: string,
@@ -176,29 +290,46 @@ async function loadClientAndServerData(
176
290
  serverEndPoint?: PageServerEndPoint,
177
291
  ): Promise<Response | void> {
178
292
  if (clientEndPoint?.load) {
179
- const clientUrl = new URL(document.location.href);
180
- const clientParams = buildClientParams(clientUrl, params, data);
181
- const clientResponse = await clientEndPoint.load(clientParams);
182
- if (clientResponse) {
183
- if (clientResponse.ok) {
184
- if (clientResponse.headers.get("Content-Type")?.includes("application/json")) {
185
- Object.assign(data, await clientResponse.json());
293
+ if (prefetchedData[location]) {
294
+ //console.log("CLIENT DATA ALREADY LOADED", location);
295
+ Object.assign(data, prefetchedData[location]);
296
+ } else {
297
+ //console.log("CLIENT DATA LOADING", location);
298
+ const clientUrl = new URL(document.location.href);
299
+ const clientParams = buildClientParams(clientUrl, params, data);
300
+ const clientResponse = await clientEndPoint.load(clientParams);
301
+ if (clientResponse) {
302
+ if (clientResponse.ok) {
303
+ if (clientResponse.headers.get("Content-Type")?.includes("application/json")) {
304
+ const clientData = await clientResponse.json();
305
+ Object.assign(data, clientData);
306
+ prefetchedData[location] = clientData;
307
+ }
308
+ } else {
309
+ return clientResponse;
186
310
  }
187
- } else {
188
- return clientResponse;
189
311
  }
190
312
  }
191
313
  }
192
314
  if (serverEndPoint?.load) {
193
315
  const serverUrl = location.replace(/\/$/, "") + "/~server";
194
- const serverResponse = await fetch(serverUrl);
195
- if (serverResponse) {
196
- if (serverResponse.ok) {
197
- if (serverResponse.headers.get("Content-Type")?.includes("application/json")) {
198
- Object.assign(data, await serverResponse.json());
316
+
317
+ if (prefetchedData[serverUrl]) {
318
+ //console.log("SERVER DATA ALREADY LOADED", serverUrl);
319
+ Object.assign(data, prefetchedData[serverUrl]);
320
+ } else {
321
+ //console.log("SERVER DATA LOADING", serverUrl);
322
+ const serverResponse = await fetch(serverUrl);
323
+ if (serverResponse) {
324
+ if (serverResponse.ok) {
325
+ if (serverResponse.headers.get("Content-Type")?.includes("application/json")) {
326
+ const serverData = await serverResponse.json();
327
+ Object.assign(data, serverData);
328
+ prefetchedData[serverUrl] = serverData;
329
+ }
330
+ } else {
331
+ return serverResponse;
199
332
  }
200
- } else {
201
- return serverResponse;
202
333
  }
203
334
  }
204
335
  }
@@ -220,8 +220,8 @@ async function loadView(
220
220
  let slots: Record<string, ServerSlotRender> | undefined = undefined;
221
221
  if (handler.layouts) {
222
222
  let slotFunctions: ServerSlotRender[] = [];
223
- slotFunctions[handler.layouts.length] = (_, context) =>
224
- (clientEndPoint.component as ServerComponent)($props, context);
223
+ slotFunctions[handler.layouts.length] = (_, $context) =>
224
+ (clientEndPoint.component as ServerComponent)($props, $context);
225
225
  for (let i = handler.layouts.length - 1; i >= 0; i--) {
226
226
  const layoutEndPoint: PageEndPoint | undefined = (await handler.layouts[i].endPoint())
227
227
  ?.default;
@@ -230,8 +230,8 @@ async function loadView(
230
230
  component = layoutEndPoint.component as ServerComponent;
231
231
  slots = { _: slotFunctions[i + 1] };
232
232
  } else {
233
- slotFunctions[i] = (_, context) =>
234
- (layoutEndPoint.component as ServerComponent)($props, context, {
233
+ slotFunctions[i] = (_, $context) =>
234
+ (layoutEndPoint.component as ServerComponent)($props, $context, {
235
235
  _: slotFunctions[i + 1],
236
236
  });
237
237
  }
@@ -300,7 +300,7 @@ async function loadClientAndServerData(
300
300
  serverEndPoint?: PageServerEndPoint,
301
301
  ) {
302
302
  if (clientEndPoint?.load) {
303
- const clientParams = buildClientParams(url, data, params);
303
+ const clientParams = buildClientParams(url, params, data);
304
304
  const clientResponse = await clientEndPoint.load(clientParams);
305
305
  if (clientResponse) {
306
306
  if (clientResponse.ok) {
@@ -13,7 +13,7 @@ type PageEndPoint = {
13
13
  /**
14
14
  * Loads data for the page.
15
15
  */
16
- load?: (event: PageLoadEvent) => any;
16
+ load?: (event: PageLoadEvent) => Response | Promise<Response | undefined> | Promise<void> | void;
17
17
  /**
18
18
  * The component that is displayed for the page.
19
19
  */