@cfdez11/vex 0.8.3 → 0.9.1
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/dist/bin/vex.js +3 -0
- package/dist/client/services/cache.js +1 -0
- package/dist/client/services/hmr-client.js +1 -0
- package/dist/client/services/html.js +1 -0
- package/dist/client/services/hydrate-client-components.js +1 -0
- package/dist/client/services/hydrate.js +1 -0
- package/dist/client/services/index.js +1 -0
- package/dist/client/services/navigation/create-layouts.js +1 -0
- package/dist/client/services/navigation/create-navigation.js +1 -0
- package/dist/client/services/navigation/index.js +1 -0
- package/dist/client/services/navigation/link-interceptor.js +1 -0
- package/dist/client/services/navigation/metadata.js +1 -0
- package/dist/client/services/navigation/navigate.js +1 -0
- package/dist/client/services/navigation/prefetch.js +1 -0
- package/dist/client/services/navigation/render-page.js +1 -0
- package/dist/client/services/navigation/render-ssr.js +1 -0
- package/dist/client/services/navigation/router.js +1 -0
- package/dist/client/services/navigation/use-query-params.js +1 -0
- package/dist/client/services/navigation/use-route-params.js +1 -0
- package/dist/client/services/navigation.js +1 -0
- package/dist/client/services/reactive.js +1 -0
- package/dist/server/build-static.js +6 -0
- package/dist/server/index.js +4 -0
- package/dist/server/prebuild.js +1 -0
- package/dist/server/utils/cache.js +1 -0
- package/dist/server/utils/component-processor.js +68 -0
- package/dist/server/utils/data-cache.js +1 -0
- package/dist/server/utils/esbuild-plugin.js +1 -0
- package/dist/server/utils/files.js +28 -0
- package/dist/server/utils/hmr.js +1 -0
- package/dist/server/utils/router.js +11 -0
- package/dist/server/utils/streaming.js +1 -0
- package/dist/server/utils/template.js +1 -0
- package/package.json +8 -7
- package/bin/vex.js +0 -69
- package/client/favicon.ico +0 -0
- package/client/services/cache.js +0 -55
- package/client/services/hmr-client.js +0 -22
- package/client/services/html.js +0 -378
- package/client/services/hydrate-client-components.js +0 -97
- package/client/services/hydrate.js +0 -25
- package/client/services/index.js +0 -9
- package/client/services/navigation/create-layouts.js +0 -172
- package/client/services/navigation/create-navigation.js +0 -103
- package/client/services/navigation/index.js +0 -8
- package/client/services/navigation/link-interceptor.js +0 -39
- package/client/services/navigation/metadata.js +0 -23
- package/client/services/navigation/navigate.js +0 -64
- package/client/services/navigation/prefetch.js +0 -43
- package/client/services/navigation/render-page.js +0 -45
- package/client/services/navigation/render-ssr.js +0 -157
- package/client/services/navigation/router.js +0 -48
- package/client/services/navigation/use-query-params.js +0 -225
- package/client/services/navigation/use-route-params.js +0 -76
- package/client/services/navigation.js +0 -6
- package/client/services/reactive.js +0 -247
- package/server/build-static.js +0 -138
- package/server/index.js +0 -135
- package/server/prebuild.js +0 -13
- package/server/utils/cache.js +0 -89
- package/server/utils/component-processor.js +0 -1631
- package/server/utils/data-cache.js +0 -62
- package/server/utils/delay.js +0 -1
- package/server/utils/esbuild-plugin.js +0 -110
- package/server/utils/files.js +0 -845
- package/server/utils/hmr.js +0 -21
- package/server/utils/router.js +0 -375
- package/server/utils/streaming.js +0 -324
- package/server/utils/template.js +0 -274
- /package/{client → dist/client}/app.webmanifest +0 -0
- /package/{server → dist/server}/root.html +0 -0
package/client/services/html.js
DELETED
|
@@ -1,378 +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
|
-
if (chain.length > 0) break; // second x-if starts a new independent chain
|
|
127
|
-
chain.push({
|
|
128
|
-
element,
|
|
129
|
-
type: 'if',
|
|
130
|
-
condition: element.getAttribute('x-if'),
|
|
131
|
-
});
|
|
132
|
-
currentIndex++;
|
|
133
|
-
} else if (element.hasAttribute('x-else-if')) {
|
|
134
|
-
if (!chain.length) break;
|
|
135
|
-
chain.push({
|
|
136
|
-
element,
|
|
137
|
-
type: 'else-if',
|
|
138
|
-
condition: element.getAttribute('x-else-if'),
|
|
139
|
-
});
|
|
140
|
-
currentIndex++;
|
|
141
|
-
} else if (element.hasAttribute('x-else')) {
|
|
142
|
-
if (!chain.length) break;
|
|
143
|
-
chain.push({
|
|
144
|
-
element,
|
|
145
|
-
type: 'else',
|
|
146
|
-
condition: null,
|
|
147
|
-
});
|
|
148
|
-
currentIndex++;
|
|
149
|
-
break; // v-else must be last
|
|
150
|
-
} else break;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Evaluate chain and keep only the first truthy element
|
|
154
|
-
let kept = null;
|
|
155
|
-
for (const item of chain) {
|
|
156
|
-
if (kept) {
|
|
157
|
-
item.element.remove();
|
|
158
|
-
continue;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
if (item.type === 'else') {
|
|
162
|
-
kept = item.element;
|
|
163
|
-
item.element.removeAttribute('x-else');
|
|
164
|
-
} else {
|
|
165
|
-
const markerIndex = markers.findIndex(m => item.condition.includes(m));
|
|
166
|
-
const condition = markerIndex !== -1 ? values[markerIndex] : false;
|
|
167
|
-
if (condition) {
|
|
168
|
-
kept = item.element;
|
|
169
|
-
item.element.removeAttribute(item.type === 'if' ? 'x-if' : 'x-else-if');
|
|
170
|
-
} else {
|
|
171
|
-
item.element.remove();
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return currentIndex - 1;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Handles x-for directives to render lists.
|
|
181
|
-
* Clones the template element for each item in the array.
|
|
182
|
-
*
|
|
183
|
-
* @param {HTMLElement} element - Template element with x-for attribute.
|
|
184
|
-
* @param {string[]} markers - Unique markers for interpolation.
|
|
185
|
-
* @param {any[]} values - Interpolated values (must include array for v-for).
|
|
186
|
-
*/
|
|
187
|
-
function handleVFor(element, markers, values) {
|
|
188
|
-
const vForValue = element.getAttribute('x-for');
|
|
189
|
-
const markerIndex = markers.findIndex(m => vForValue.includes(m));
|
|
190
|
-
if (markerIndex === -1 || !Array.isArray(values[markerIndex])) {
|
|
191
|
-
element.removeAttribute('x-for');
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const items = values[markerIndex];
|
|
196
|
-
const parent = element.parentNode;
|
|
197
|
-
const template = element.cloneNode(true);
|
|
198
|
-
template.removeAttribute('x-for');
|
|
199
|
-
|
|
200
|
-
const fragment = document.createDocumentFragment();
|
|
201
|
-
for (const item of items) {
|
|
202
|
-
const clone = template.cloneNode(true);
|
|
203
|
-
replaceItemReferences(clone, item);
|
|
204
|
-
fragment.appendChild(clone);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
parent.replaceChild(fragment, element);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Replaces item references in cloned v-for elements.
|
|
212
|
-
*
|
|
213
|
-
* @param {Node} node - Node to replace references in.
|
|
214
|
-
* @param {Object} item - Current item from the array.
|
|
215
|
-
*/
|
|
216
|
-
function replaceItemReferences(node, item) {
|
|
217
|
-
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
218
|
-
for (const attr of Array.from(node.attributes)) {
|
|
219
|
-
if (attr.value.includes('item.')) {
|
|
220
|
-
const prop = attr.value.replace('item.', '');
|
|
221
|
-
node.setAttribute(attr.name, item[prop] ?? '');
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
for (const child of Array.from(node.childNodes)) {
|
|
227
|
-
replaceItemReferences(child, item)
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* Recursively processes nodes, replacing markers with actual values.
|
|
233
|
-
* Also handles attributes and event listeners.
|
|
234
|
-
*
|
|
235
|
-
* @param {Node} node - Node to process.
|
|
236
|
-
* @param {string[]} markers - Unique markers for interpolation.
|
|
237
|
-
* @param {any[]} values - Interpolated values.
|
|
238
|
-
*/
|
|
239
|
-
function processNode(node, markers, values) {
|
|
240
|
-
if (node.nodeType === Node.TEXT_NODE) {
|
|
241
|
-
return processTextNode(node, markers, values)
|
|
242
|
-
};
|
|
243
|
-
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
244
|
-
processAttributes(node, markers, values);
|
|
245
|
-
}
|
|
246
|
-
for (const child of Array.from(node.childNodes)) {
|
|
247
|
-
processNode(child, markers, values);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* Processes text nodes by replacing markers with values.
|
|
253
|
-
* Supports primitives, nodes, and arrays of nodes.
|
|
254
|
-
*
|
|
255
|
-
* @param {Text} node - Text node to process.
|
|
256
|
-
* @param {string[]} markers - Unique markers for interpolation.
|
|
257
|
-
* @param {any[]} values - Interpolated values.
|
|
258
|
-
*/
|
|
259
|
-
function processTextNode(node, markers, values) {
|
|
260
|
-
let text = node.textContent;
|
|
261
|
-
|
|
262
|
-
for (let i = 0; i < markers.length; i++) {
|
|
263
|
-
if (!text.includes(markers[i])) {
|
|
264
|
-
continue;
|
|
265
|
-
}
|
|
266
|
-
const value = values[i];
|
|
267
|
-
const parent = node.parentNode;
|
|
268
|
-
const parts = text.split(markers[i]);
|
|
269
|
-
|
|
270
|
-
if (parts[0]) {
|
|
271
|
-
parent.insertBefore(document.createTextNode(parts[0]), node);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
if (Array.isArray(value)) {
|
|
275
|
-
// Insert arrays of nodes or primitives
|
|
276
|
-
for (const item of value) {
|
|
277
|
-
if (item instanceof Node) {
|
|
278
|
-
processNode(item, markers, values);
|
|
279
|
-
parent.insertBefore(item, node);
|
|
280
|
-
} else {
|
|
281
|
-
parent.insertBefore(document.createTextNode(String(item ?? "")), node);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
} else if (value instanceof Node) {
|
|
285
|
-
processNode(value, markers, values);
|
|
286
|
-
parent.insertBefore(value, node);
|
|
287
|
-
} else {
|
|
288
|
-
parent.insertBefore(document.createTextNode(String(value ?? "")), node);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
text = parts.slice(1).join(markers[i]);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
node.textContent = text;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* Maps special HTML attributes to DOM properties.
|
|
299
|
-
*
|
|
300
|
-
* @param {string} attrName - Attribute name from template.
|
|
301
|
-
* @returns {object|string} - Property name and joinability or original string.
|
|
302
|
-
*/
|
|
303
|
-
function getNodePropertyInfo(attrName) {
|
|
304
|
-
const nodeProperties = {
|
|
305
|
-
class: { property: "className", canBeJoined: true }
|
|
306
|
-
};
|
|
307
|
-
return nodeProperties[attrName] || { property: attrName, canBeJoined: false };
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* Processes element attributes.
|
|
312
|
-
* Supports:
|
|
313
|
-
* - @event bindings: adds native DOM event listeners.
|
|
314
|
-
* - :prop bindings: sets DOM properties and boolean attributes.
|
|
315
|
-
*
|
|
316
|
-
* @param {HTMLElement} element - Element to process.
|
|
317
|
-
* @param {string[]} markers - Interpolation markers.
|
|
318
|
-
* @param {any[]} values - Interpolated values.
|
|
319
|
-
*/
|
|
320
|
-
function processAttributes(element, markers, values) {
|
|
321
|
-
for (const attr of Array.from(element.attributes)) {
|
|
322
|
-
// Event binding: @event
|
|
323
|
-
if (attr.name.startsWith("@")) {
|
|
324
|
-
const event = attr.name.slice(1);
|
|
325
|
-
const idx = markers.findIndex(m => attr.value.includes(m));
|
|
326
|
-
const handler = values[idx];
|
|
327
|
-
|
|
328
|
-
if (typeof handler === "function") {
|
|
329
|
-
element.addEventListener(event, handler);
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
element.removeAttribute(attr.name);
|
|
333
|
-
continue;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
// Property/boolean binding: :prop
|
|
337
|
-
if (attr.name.startsWith(":")) {
|
|
338
|
-
const { property, canBeJoined } = getNodePropertyInfo(attr.name.slice(1));
|
|
339
|
-
const idx = markers.findIndex((m) => attr.value.includes(m));
|
|
340
|
-
|
|
341
|
-
if (idx !== -1) {
|
|
342
|
-
const value = values[idx];
|
|
343
|
-
if (typeof value === "boolean") {
|
|
344
|
-
element.toggleAttribute(property, value);
|
|
345
|
-
}
|
|
346
|
-
else {
|
|
347
|
-
element[property] = canBeJoined && element[property]
|
|
348
|
-
? `${element[property]} ${value}`
|
|
349
|
-
: value;
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
element.removeAttribute(attr.name);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
// x-show directive
|
|
357
|
-
if (attr.name === "x-show") {
|
|
358
|
-
const idx = markers.findIndex((m) => attr.value.includes(m));
|
|
359
|
-
const value = idx !== -1 ? values[idx] : false;
|
|
360
|
-
element.style.display = value ? "" : "none";
|
|
361
|
-
element.removeAttribute("x-show");
|
|
362
|
-
continue;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// data set attributes
|
|
366
|
-
if(attr.name.startsWith("data-")) {
|
|
367
|
-
const dataAttr = attr.name.slice(5);
|
|
368
|
-
const idx = markers.findIndex((m) => attr.value.includes(m));
|
|
369
|
-
|
|
370
|
-
if (idx !== -1) {
|
|
371
|
-
const value = values[idx];
|
|
372
|
-
element.dataset[dataAttr] = typeof value === "object" && value !== null
|
|
373
|
-
? JSON.stringify(value)
|
|
374
|
-
: String(value ?? "");
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
}
|
|
@@ -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
|
-
};
|
package/client/services/index.js
DELETED
|
@@ -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
|
-
}
|