@cfdez11/vex 0.8.2 → 0.9.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 (71) hide show
  1. package/dist/bin/vex.js +3 -0
  2. package/dist/client/services/cache.js +1 -0
  3. package/dist/client/services/hmr-client.js +1 -0
  4. package/dist/client/services/html.js +1 -0
  5. package/dist/client/services/hydrate-client-components.js +1 -0
  6. package/dist/client/services/hydrate.js +1 -0
  7. package/dist/client/services/index.js +1 -0
  8. package/dist/client/services/navigation/create-layouts.js +1 -0
  9. package/dist/client/services/navigation/create-navigation.js +1 -0
  10. package/dist/client/services/navigation/index.js +1 -0
  11. package/dist/client/services/navigation/link-interceptor.js +1 -0
  12. package/dist/client/services/navigation/metadata.js +1 -0
  13. package/dist/client/services/navigation/navigate.js +1 -0
  14. package/dist/client/services/navigation/prefetch.js +1 -0
  15. package/dist/client/services/navigation/render-page.js +1 -0
  16. package/dist/client/services/navigation/render-ssr.js +1 -0
  17. package/dist/client/services/navigation/router.js +1 -0
  18. package/dist/client/services/navigation/use-query-params.js +1 -0
  19. package/dist/client/services/navigation/use-route-params.js +1 -0
  20. package/dist/client/services/navigation.js +1 -0
  21. package/dist/client/services/reactive.js +1 -0
  22. package/dist/server/build-static.js +6 -0
  23. package/dist/server/index.js +4 -0
  24. package/dist/server/prebuild.js +1 -0
  25. package/dist/server/utils/cache.js +1 -0
  26. package/dist/server/utils/component-processor.js +68 -0
  27. package/dist/server/utils/data-cache.js +1 -0
  28. package/dist/server/utils/esbuild-plugin.js +1 -0
  29. package/dist/server/utils/files.js +28 -0
  30. package/dist/server/utils/hmr.js +1 -0
  31. package/dist/server/utils/router.js +11 -0
  32. package/dist/server/utils/streaming.js +1 -0
  33. package/dist/server/utils/template.js +1 -0
  34. package/package.json +8 -7
  35. package/bin/vex.js +0 -69
  36. package/client/favicon.ico +0 -0
  37. package/client/services/cache.js +0 -55
  38. package/client/services/hmr-client.js +0 -22
  39. package/client/services/html.js +0 -377
  40. package/client/services/hydrate-client-components.js +0 -97
  41. package/client/services/hydrate.js +0 -25
  42. package/client/services/index.js +0 -9
  43. package/client/services/navigation/create-layouts.js +0 -172
  44. package/client/services/navigation/create-navigation.js +0 -103
  45. package/client/services/navigation/index.js +0 -8
  46. package/client/services/navigation/link-interceptor.js +0 -39
  47. package/client/services/navigation/metadata.js +0 -23
  48. package/client/services/navigation/navigate.js +0 -64
  49. package/client/services/navigation/prefetch.js +0 -43
  50. package/client/services/navigation/render-page.js +0 -45
  51. package/client/services/navigation/render-ssr.js +0 -157
  52. package/client/services/navigation/router.js +0 -48
  53. package/client/services/navigation/use-query-params.js +0 -225
  54. package/client/services/navigation/use-route-params.js +0 -76
  55. package/client/services/navigation.js +0 -6
  56. package/client/services/reactive.js +0 -247
  57. package/server/build-static.js +0 -138
  58. package/server/index.js +0 -135
  59. package/server/prebuild.js +0 -13
  60. package/server/utils/cache.js +0 -89
  61. package/server/utils/component-processor.js +0 -1631
  62. package/server/utils/data-cache.js +0 -62
  63. package/server/utils/delay.js +0 -1
  64. package/server/utils/esbuild-plugin.js +0 -110
  65. package/server/utils/files.js +0 -845
  66. package/server/utils/hmr.js +0 -21
  67. package/server/utils/router.js +0 -375
  68. package/server/utils/streaming.js +0 -324
  69. package/server/utils/template.js +0 -274
  70. /package/{client → dist/client}/app.webmanifest +0 -0
  71. /package/{server → dist/server}/root.html +0 -0
@@ -1,377 +0,0 @@
1
- /**
2
- * Tagged template literal to create HTML elements with Vue-like directives.
3
- *
4
- * Supported syntax:
5
- *
6
- * 1. Text interpolation:
7
- * html`<span>${value}</span>`
8
- *
9
- * 2. Attribute interpolation:
10
- * html`<div class="${className}" id="${id}"></div>`
11
- *
12
- * 3. Event bindings (@event):
13
- * html`<button @click="${handler}">Click</button>`
14
- *
15
- * 4. Property/Boolean bindings (:prop):
16
- * html`<button :disabled="${isDisabled}">Send</button>`
17
- *
18
- * 5. Conditional rendering (x-if, x-else-if, x-else):
19
- * html`<div x-if="${condition}">Show if true</div>`
20
- * html`<div x-else>Show if false</div>`
21
- *
22
- * 6. Loop rendering (x-for):
23
- * html`<li x-for="${item => items}">Item: ${item}</li>`
24
- *
25
- * 7. Nested templates and arrays:
26
- * html`<div>${items.map(item => html`<li>${item}</li>`)}</div>`
27
- */
28
-
29
- /**
30
- * Main template literal function for creating DOM elements.
31
- * Processes directives, text interpolation, attributes, and events.
32
- *
33
- * @param {TemplateStringsArray} strings - The literal strings from the template.
34
- * @param {...any} values - Interpolated values, can be primitives, arrays, or nodes.
35
- * @returns {HTMLElement | DocumentFragment} - The rendered DOM node(s).
36
- */
37
- export function html(strings, ...values) {
38
- // Generate unique markers for interpolation positions
39
- const markers = values.map((_, i) => `__HTML_MARKER_${i}__`);
40
-
41
- // Combine template strings and markers to form HTML string
42
- let htmlString = strings[0];
43
- for (let i = 0; i < values.length; i++) {
44
- htmlString += markers[i] + strings[i + 1];
45
- }
46
-
47
- // Create a template element to parse HTML
48
- const template = document.createElement("template");
49
- template.innerHTML = htmlString.trim();
50
-
51
- // Clone content to avoid mutating the template
52
- const fragment = template.content.cloneNode(true);
53
-
54
- // Process VexJS directives (x-if, x-else-if, x-else, x-for)
55
- processDirectives(fragment, markers, values);
56
-
57
- // Determine single root element or return a fragment
58
- const node =
59
- fragment.childElementCount === 1
60
- ? fragment.firstElementChild
61
- : fragment;
62
-
63
- // Process text interpolations, attributes, and event bindings
64
- processNode(node, markers, values);
65
-
66
- return node;
67
- }
68
-
69
- /**
70
- * Recursively processes directives on a node and its children.
71
- * Supports x-if, x-else-if, x-else, and x-for.
72
- *
73
- * @param {Node} node - The DOM node or fragment to process.
74
- * @param {string[]} markers - Unique markers for interpolated values.
75
- * @param {any[]} values - Interpolated values.
76
- */
77
- function processDirectives(node, markers, values) {
78
- if (node.nodeType !== Node.ELEMENT_NODE && node.nodeType !== Node.DOCUMENT_FRAGMENT_NODE) return;
79
-
80
- const children = Array.from(node.childNodes);
81
-
82
- for (let i = 0; i < children.length; i++) {
83
- const child = children[i];
84
-
85
- if (child.nodeType === Node.ELEMENT_NODE) {
86
- if (child.hasAttribute('x-if')) {
87
- // skip all processed nodes in the conditional chain
88
- i = handleConditionalChain(node, children, i, markers, values);
89
- continue;
90
- }
91
-
92
- if (child.hasAttribute('x-for')) {
93
- handleVFor(child, markers, values);
94
- continue;
95
- }
96
-
97
- processDirectives(child, markers, values);
98
- }
99
- }
100
- }
101
-
102
- /**
103
- * Processes x-if, x-else-if, and x-else chains.
104
- * Keeps the first element whose condition is truthy.
105
- *
106
- * @param {Node} parent - Parent node of the conditional chain.
107
- * @param {Node[]} children - List of child nodes.
108
- * @param {number} startIndex - Index where v-if chain starts.
109
- * @param {string[]} markers - Unique markers for interpolation.
110
- * @param {any[]} values - Interpolated values.
111
- * @returns {number} - Updated index after processing chain.
112
- */
113
- function handleConditionalChain(parent, children, startIndex, markers, values) {
114
- const chain = [];
115
- let currentIndex = startIndex;
116
-
117
- // Collect all conditional elements in the chain
118
- while (currentIndex < children.length) {
119
- const element = children[currentIndex];
120
- if (element.nodeType !== Node.ELEMENT_NODE) {
121
- currentIndex++;
122
- continue;
123
- }
124
-
125
- if (element.hasAttribute('x-if')) {
126
- chain.push({
127
- element,
128
- type: 'if',
129
- condition: element.getAttribute('x-if'),
130
- });
131
- currentIndex++;
132
- } else if (element.hasAttribute('x-else-if')) {
133
- if (!chain.length) break;
134
- chain.push({
135
- element,
136
- type: 'else-if',
137
- condition: element.getAttribute('x-else-if'),
138
- });
139
- currentIndex++;
140
- } else if (element.hasAttribute('x-else')) {
141
- if (!chain.length) break;
142
- chain.push({
143
- element,
144
- type: 'else',
145
- condition: null,
146
- });
147
- currentIndex++;
148
- break; // v-else must be last
149
- } else break;
150
- }
151
-
152
- // Evaluate chain and keep only the first truthy element
153
- let kept = null;
154
- for (const item of chain) {
155
- if (kept) {
156
- item.element.remove();
157
- continue;
158
- }
159
-
160
- if (item.type === 'else') {
161
- kept = item.element;
162
- item.element.removeAttribute('x-else');
163
- } else {
164
- const markerIndex = markers.findIndex(m => item.condition.includes(m));
165
- const condition = markerIndex !== -1 ? values[markerIndex] : false;
166
- if (condition) {
167
- kept = item.element;
168
- item.element.removeAttribute(item.type === 'if' ? 'x-if' : 'x-else-if');
169
- } else {
170
- item.element.remove();
171
- }
172
- }
173
- }
174
-
175
- return currentIndex - 1;
176
- }
177
-
178
- /**
179
- * Handles x-for directives to render lists.
180
- * Clones the template element for each item in the array.
181
- *
182
- * @param {HTMLElement} element - Template element with x-for attribute.
183
- * @param {string[]} markers - Unique markers for interpolation.
184
- * @param {any[]} values - Interpolated values (must include array for v-for).
185
- */
186
- function handleVFor(element, markers, values) {
187
- const vForValue = element.getAttribute('x-for');
188
- const markerIndex = markers.findIndex(m => vForValue.includes(m));
189
- if (markerIndex === -1 || !Array.isArray(values[markerIndex])) {
190
- element.removeAttribute('x-for');
191
- return;
192
- }
193
-
194
- const items = values[markerIndex];
195
- const parent = element.parentNode;
196
- const template = element.cloneNode(true);
197
- template.removeAttribute('x-for');
198
-
199
- const fragment = document.createDocumentFragment();
200
- for (const item of items) {
201
- const clone = template.cloneNode(true);
202
- replaceItemReferences(clone, item);
203
- fragment.appendChild(clone);
204
- }
205
-
206
- parent.replaceChild(fragment, element);
207
- }
208
-
209
- /**
210
- * Replaces item references in cloned v-for elements.
211
- *
212
- * @param {Node} node - Node to replace references in.
213
- * @param {Object} item - Current item from the array.
214
- */
215
- function replaceItemReferences(node, item) {
216
- if (node.nodeType === Node.ELEMENT_NODE) {
217
- for (const attr of Array.from(node.attributes)) {
218
- if (attr.value.includes('item.')) {
219
- const prop = attr.value.replace('item.', '');
220
- node.setAttribute(attr.name, item[prop] ?? '');
221
- }
222
- }
223
- }
224
-
225
- for (const child of Array.from(node.childNodes)) {
226
- replaceItemReferences(child, item)
227
- };
228
- }
229
-
230
- /**
231
- * Recursively processes nodes, replacing markers with actual values.
232
- * Also handles attributes and event listeners.
233
- *
234
- * @param {Node} node - Node to process.
235
- * @param {string[]} markers - Unique markers for interpolation.
236
- * @param {any[]} values - Interpolated values.
237
- */
238
- function processNode(node, markers, values) {
239
- if (node.nodeType === Node.TEXT_NODE) {
240
- return processTextNode(node, markers, values)
241
- };
242
- if (node.nodeType === Node.ELEMENT_NODE) {
243
- processAttributes(node, markers, values);
244
- }
245
- for (const child of Array.from(node.childNodes)) {
246
- processNode(child, markers, values);
247
- }
248
- }
249
-
250
- /**
251
- * Processes text nodes by replacing markers with values.
252
- * Supports primitives, nodes, and arrays of nodes.
253
- *
254
- * @param {Text} node - Text node to process.
255
- * @param {string[]} markers - Unique markers for interpolation.
256
- * @param {any[]} values - Interpolated values.
257
- */
258
- function processTextNode(node, markers, values) {
259
- let text = node.textContent;
260
-
261
- for (let i = 0; i < markers.length; i++) {
262
- if (!text.includes(markers[i])) {
263
- continue;
264
- }
265
- const value = values[i];
266
- const parent = node.parentNode;
267
- const parts = text.split(markers[i]);
268
-
269
- if (parts[0]) {
270
- parent.insertBefore(document.createTextNode(parts[0]), node);
271
- }
272
-
273
- if (Array.isArray(value)) {
274
- // Insert arrays of nodes or primitives
275
- for (const item of value) {
276
- if (item instanceof Node) {
277
- processNode(item, markers, values);
278
- parent.insertBefore(item, node);
279
- } else {
280
- parent.insertBefore(document.createTextNode(String(item ?? "")), node);
281
- }
282
- }
283
- } else if (value instanceof Node) {
284
- processNode(value, markers, values);
285
- parent.insertBefore(value, node);
286
- } else {
287
- parent.insertBefore(document.createTextNode(String(value ?? "")), node);
288
- }
289
-
290
- text = parts.slice(1).join(markers[i]);
291
- }
292
-
293
- node.textContent = text;
294
- }
295
-
296
- /**
297
- * Maps special HTML attributes to DOM properties.
298
- *
299
- * @param {string} attrName - Attribute name from template.
300
- * @returns {object|string} - Property name and joinability or original string.
301
- */
302
- function getNodePropertyInfo(attrName) {
303
- const nodeProperties = {
304
- class: { property: "className", canBeJoined: true }
305
- };
306
- return nodeProperties[attrName] || { property: attrName, canBeJoined: false };
307
- }
308
-
309
- /**
310
- * Processes element attributes.
311
- * Supports:
312
- * - @event bindings: adds native DOM event listeners.
313
- * - :prop bindings: sets DOM properties and boolean attributes.
314
- *
315
- * @param {HTMLElement} element - Element to process.
316
- * @param {string[]} markers - Interpolation markers.
317
- * @param {any[]} values - Interpolated values.
318
- */
319
- function processAttributes(element, markers, values) {
320
- for (const attr of Array.from(element.attributes)) {
321
- // Event binding: @event
322
- if (attr.name.startsWith("@")) {
323
- const event = attr.name.slice(1);
324
- const idx = markers.findIndex(m => attr.value.includes(m));
325
- const handler = values[idx];
326
-
327
- if (typeof handler === "function") {
328
- element.addEventListener(event, handler);
329
- }
330
-
331
- element.removeAttribute(attr.name);
332
- continue;
333
- }
334
-
335
- // Property/boolean binding: :prop
336
- if (attr.name.startsWith(":")) {
337
- const { property, canBeJoined } = getNodePropertyInfo(attr.name.slice(1));
338
- const idx = markers.findIndex((m) => attr.value.includes(m));
339
-
340
- if (idx !== -1) {
341
- const value = values[idx];
342
- if (typeof value === "boolean") {
343
- element.toggleAttribute(property, value);
344
- }
345
- else {
346
- element[property] = canBeJoined && element[property]
347
- ? `${element[property]} ${value}`
348
- : value;
349
- }
350
- }
351
-
352
- element.removeAttribute(attr.name);
353
- }
354
-
355
- // x-show directive
356
- if (attr.name === "x-show") {
357
- const idx = markers.findIndex((m) => attr.value.includes(m));
358
- const value = idx !== -1 ? values[idx] : false;
359
- element.style.display = value ? "" : "none";
360
- element.removeAttribute("x-show");
361
- continue;
362
- }
363
-
364
- // data set attributes
365
- if(attr.name.startsWith("data-")) {
366
- const dataAttr = attr.name.slice(5);
367
- const idx = markers.findIndex((m) => attr.value.includes(m));
368
-
369
- if (idx !== -1) {
370
- const value = values[idx];
371
- element.dataset[dataAttr] = typeof value === "object" && value !== null
372
- ? JSON.stringify(value)
373
- : String(value ?? "");
374
- }
375
- }
376
- }
377
- }
@@ -1,97 +0,0 @@
1
- /**
2
- * Client-side component hydration script.
3
- *
4
- * This script automatically hydrates all components marked with
5
- * `data-client:component` in the DOM. It supports:
6
- * - Initial hydration on page load
7
- * - Progressive hydration for streaming SSR content
8
- * - SPA updates by exposing a global `window.hydrateComponents` function
9
- *
10
- * Each component module is dynamically imported, and its exported
11
- * `hydrateClientComponent` function is called with the component marker.
12
- */
13
- (function () {
14
-
15
- /**
16
- * Hydrates a single component marker.
17
- *
18
- * This function checks if the marker is already hydrated via the
19
- * `data-hydrated` attribute to avoid rehydration. It dynamically imports
20
- * the component module and calls its `hydrateClientComponent` function.
21
- *
22
- * @param {HTMLElement} marker - The <template> or marker element representing a client component.
23
- * @param {Object} [props={}] - Optional props to pass to the client component.
24
- */
25
- async function hydrateMarker(marker, props = {}) {
26
- if (marker.dataset.hydrated === "true") return;
27
- marker.dataset.hydrated = "true";
28
-
29
- const componentName = marker.getAttribute("data-client:component");
30
- const componentProps = marker.getAttribute("data-client:props");
31
-
32
- let parsedProps = {};
33
- try {
34
- parsedProps = JSON.parse(componentProps || "{}");
35
- } catch (e) {
36
- console.warn(`Failed to parse props for component ${componentName}`, e);
37
- }
38
- const finalProps = { ...parsedProps, ...props };
39
-
40
- try {
41
- const module = await import(`/_vexjs/_components/${componentName}.js`);
42
- await module.hydrateClientComponent(marker, finalProps);
43
- } catch (error) {
44
- console.error(`Failed to load component: ${componentName}`, error);
45
- }
46
- }
47
-
48
- /**
49
- * Hydrates all unhydrated component markers inside a container.
50
- *
51
- * @param {HTMLElement|Document} [container=document] - The root container to scan for components.
52
- */
53
- async function hydrateComponents(container = document, props = {}) {
54
- const markers = container.querySelectorAll(
55
- "[data-client\\:component]:not([data-hydrated='true'])"
56
- );
57
-
58
- for (const marker of markers) {
59
- await hydrateMarker(marker, props);
60
- }
61
- }
62
-
63
- /**
64
- * MutationObserver callback for progressive hydration.
65
- *
66
- * Observes DOM mutations and hydrates newly added components dynamically.
67
- */
68
- const observer = new MutationObserver((mutations) => {
69
- for (const mutation of mutations) {
70
- for (const node of mutation.addedNodes) {
71
- if (node.nodeType !== 1) continue; // Only element nodes
72
- if (node.matches?.("[data-client\\:component]")) hydrateMarker(node);
73
- hydrateComponents(node);
74
- }
75
- }
76
- });
77
-
78
- // Start observing the document for new nodes
79
- observer.observe(document, { childList: true, subtree: true });
80
-
81
- // Hydrate existing components on DOMContentLoaded or immediately if already interactive.
82
- // The observer is intentionally NOT disconnected here — it must stay active to catch
83
- // components inserted after DOMContentLoaded (nested CSR components, Suspense streaming,
84
- // SPA navigations). The `data-hydrated` guard in hydrateMarker prevents double-hydration.
85
- if (document.readyState === "loading") {
86
- document.addEventListener("DOMContentLoaded", () => hydrateComponents());
87
- } else {
88
- hydrateComponents();
89
- }
90
-
91
- /**
92
- * Expose `hydrateComponents` globally so it can be called manually
93
- * for SPA navigations or dynamically rendered content.
94
- * @type {function(HTMLElement|Document): Promise<void>}
95
- */
96
- window.hydrateComponents = hydrateComponents;
97
- })();
@@ -1,25 +0,0 @@
1
- /**
2
- * Client-side hydration helper for streaming Suspense boundaries.
3
- *
4
- * Previously this script was injected as a separate <script src="hydrate.js">
5
- * tag for every Suspense boundary on the page. The browser cached the file after
6
- * the first load, but still had to parse and initialise a new script execution
7
- * context for each tag — O(N) work per page with N Suspense boundaries.
8
- *
9
- * Now the script is loaded exactly once from root.html and exposes
10
- * `window.hydrateTarget(targetId, sourceId)`. Each Suspense replacement payload
11
- * calls that global function via a tiny inline script instead of loading this
12
- * file again.
13
- *
14
- * @param {string} targetId - ID of the fallback <div> to replace.
15
- * @param {string} sourceId - ID of the <template> containing the real content.
16
- */
17
- window.hydrateTarget = function (targetId, sourceId) {
18
- const target = document.getElementById(targetId);
19
- const template = document.getElementById(sourceId);
20
-
21
- if (target && template) {
22
- target.replaceWith(template.content.cloneNode(true));
23
- template.remove();
24
- }
25
- };
@@ -1,9 +0,0 @@
1
- import { initializeRouter, navigate } from "./navigation/index.js";
2
-
3
- window.app = {
4
- navigate,
5
- };
6
-
7
- document.addEventListener("DOMContentLoaded", () => {
8
- initializeRouter();
9
- });
@@ -1,172 +0,0 @@
1
- /**
2
- * @typedef {Object} Layout
3
- * @property {string} name - Layout name.
4
- * @property {string} importPath - Path to the module.
5
- */
6
-
7
- /**
8
- * @typedef {Object} RenderedLayout
9
- * @property {string} name - Layout name.
10
- * @property {Node} children - Original children node.
11
- * @property {Node} node - Rendered layout node.
12
- */
13
-
14
- /**
15
- * @typedef {Object} GenerateParams
16
- * @property {Layout[]} [routeLayouts] - Layouts to render for this route.
17
- * @property {Node} pageNode - The page node to wrap.
18
- * @property {any} metadata - Metadata for the page/layout.
19
- */
20
-
21
- /**
22
- * @typedef {Object} GenerateResult
23
- * @property {string | null} layoutId - ID of the nearest rendered layout.
24
- * @property {Node} node - The root node after layout wrapping.
25
- * @property {any} metadata - Metadata after merging layouts.
26
- */
27
-
28
- /**
29
- * Creates a layout renderer responsible for dynamically loading,
30
- * rendering, caching, and patching route-based layouts.
31
- *
32
- * The renderer keeps track of already rendered layouts to avoid
33
- * unnecessary re-renders and supports incremental layout updates.
34
- *
35
- * @returns {{
36
- * generate: (params: GenerateParams) => Promise<GenerateResult>,
37
- * patch: (layoutId: string, node: Node) => void,
38
- * reset: () => void
39
- * }}
40
- */
41
- export function createLayoutRenderer() {
42
- /** @type {Map<string, RenderedLayout>} */
43
- const renderedLayouts = new Map();
44
-
45
- /**
46
- * Removes cached layouts that are no longer part of the current route.
47
- * @param {Layout[]} routeLayouts
48
- */
49
- function cleanNotNeeded(routeLayouts) {
50
- for (const name of renderedLayouts.keys()) {
51
- const exists = routeLayouts.some((l) => l.name === name);
52
- if (!exists) {
53
- renderedLayouts.delete(name);
54
- }
55
- }
56
- }
57
-
58
- /**
59
- * Finds the nearest already-rendered layout in the route hierarchy.
60
- * @param {Layout[]} routeLayouts
61
- * @returns {RenderedLayout | null}
62
- */
63
- function getNearestRendered(routeLayouts) {
64
- const reversed = routeLayouts.toReversed();
65
-
66
- for (const layout of reversed) {
67
- if (renderedLayouts.has(layout.name)) {
68
- return renderedLayouts.get(layout.name);
69
- }
70
- }
71
-
72
- return null;
73
- }
74
-
75
- /**
76
- * Determines which layouts need to be rendered based on
77
- * the nearest cached layout.
78
- * @param {Layout[]} routeLayouts
79
- * @param {RenderedLayout | null} nearestRendered
80
- * @returns {Layout[]}
81
- */
82
- function getLayoutsToRender(routeLayouts, nearestRendered) {
83
- if (!nearestRendered) return routeLayouts;
84
-
85
- const reversed = routeLayouts.toReversed();
86
- const idx = reversed.findIndex((l) => l.name === nearestRendered.name);
87
-
88
- return idx === -1 ? routeLayouts : reversed.slice(0, idx);
89
- }
90
-
91
- /**
92
- * Dynamically imports layout modules.
93
- * @param {Layout[]} layouts
94
- * @returns {Promise<any[]>}
95
- */
96
- async function loadLayoutModules(layouts) {
97
- return Promise.all(layouts.map((layout) => import(layout.importPath)));
98
- }
99
-
100
- /**
101
- * Generates the layout tree wrapping the provided page node.
102
- * @param {GenerateParams} params
103
- * @returns {Promise<GenerateResult>}
104
- */
105
- async function generate({ routeLayouts = [], pageNode, metadata }) {
106
- if (!pageNode || routeLayouts.length === 0) {
107
- return {
108
- layoutId: null,
109
- node: pageNode,
110
- metadata,
111
- };
112
- }
113
-
114
- cleanNotNeeded(routeLayouts);
115
-
116
- const nearestRendered = getNearestRendered(routeLayouts);
117
- const layoutsToRender = getLayoutsToRender(routeLayouts, nearestRendered);
118
-
119
- const modules = await loadLayoutModules(layoutsToRender);
120
-
121
- let htmlContainerNode = pageNode;
122
- let deepestMetadata = metadata;
123
-
124
- for (let i = modules.length - 1; i >= 0; i--) {
125
- const layout = layoutsToRender[i];
126
- const mod = modules[i];
127
-
128
- const children = htmlContainerNode;
129
- const marker = document.createElement("template");
130
-
131
- htmlContainerNode = mod.hydrateClientComponent(marker, { children });
132
-
133
- if (!deepestMetadata && mod.metadata) {
134
- deepestMetadata = mod.metadata;
135
- }
136
-
137
- renderedLayouts.set(layout.name, {
138
- name: layout.name,
139
- children,
140
- node: htmlContainerNode,
141
- });
142
- }
143
-
144
- return {
145
- layoutId: nearestRendered?.name ?? null,
146
- node: htmlContainerNode,
147
- metadata: deepestMetadata,
148
- };
149
- }
150
-
151
- /**
152
- * Patches an already-rendered layout by replacing its children node.
153
- * @param {string} layoutId
154
- * @param {Node} node
155
- */
156
- function patch(layoutId, node) {
157
- const record = renderedLayouts.get(layoutId);
158
- if (!record) return;
159
-
160
- record.children.replaceWith(node);
161
- record.children = node;
162
- }
163
-
164
- /**
165
- * Clears all cached rendered layouts.
166
- */
167
- function reset() {
168
- renderedLayouts.clear();
169
- }
170
-
171
- return { generate, patch, reset };
172
- }