@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 +13 -0
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/package.json +5 -5
- package/src/site/clientEntry.ts +191 -60
- package/src/site/serverEntry.ts +5 -5
- package/src/types/PageEndPoint.ts +1 -1
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) =>
|
|
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) =>
|
|
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.
|
|
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": "^
|
|
56
|
+
"vite": "^7.0.0",
|
|
57
57
|
"vite-tsconfig-paths": "^5.1.4",
|
|
58
|
-
"@torpor/
|
|
59
|
-
"@torpor/
|
|
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.
|
|
66
|
+
"vitest": "^3.2.4"
|
|
67
67
|
},
|
|
68
68
|
"scripts": {
|
|
69
69
|
"build": "tsc --noEmit && tsup",
|
package/src/site/clientEntry.ts
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
91
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
|
|
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
|
-
|
|
133
|
-
|
|
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
|
|
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
|
|
144
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
195
|
-
if (
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
}
|
package/src/site/serverEntry.ts
CHANGED
|
@@ -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,
|
|
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) =>
|
|
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
|
*/
|