@cfdez11/vex 0.1.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 (35) hide show
  1. package/README.md +1383 -0
  2. package/client/app.webmanifest +14 -0
  3. package/client/favicon.ico +0 -0
  4. package/client/services/cache.js +55 -0
  5. package/client/services/hmr-client.js +22 -0
  6. package/client/services/html.js +377 -0
  7. package/client/services/hydrate-client-components.js +97 -0
  8. package/client/services/hydrate.js +25 -0
  9. package/client/services/index.js +9 -0
  10. package/client/services/navigation/create-layouts.js +172 -0
  11. package/client/services/navigation/create-navigation.js +103 -0
  12. package/client/services/navigation/index.js +8 -0
  13. package/client/services/navigation/link-interceptor.js +39 -0
  14. package/client/services/navigation/metadata.js +23 -0
  15. package/client/services/navigation/navigate.js +64 -0
  16. package/client/services/navigation/prefetch.js +43 -0
  17. package/client/services/navigation/render-page.js +45 -0
  18. package/client/services/navigation/render-ssr.js +157 -0
  19. package/client/services/navigation/router.js +48 -0
  20. package/client/services/navigation/use-query-params.js +225 -0
  21. package/client/services/navigation/use-route-params.js +76 -0
  22. package/client/services/reactive.js +231 -0
  23. package/package.json +24 -0
  24. package/server/index.js +115 -0
  25. package/server/prebuild.js +12 -0
  26. package/server/root.html +15 -0
  27. package/server/utils/cache.js +89 -0
  28. package/server/utils/component-processor.js +1526 -0
  29. package/server/utils/data-cache.js +62 -0
  30. package/server/utils/delay.js +1 -0
  31. package/server/utils/files.js +723 -0
  32. package/server/utils/hmr.js +21 -0
  33. package/server/utils/router.js +373 -0
  34. package/server/utils/streaming.js +315 -0
  35. package/server/utils/template.js +263 -0
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "ReelingIt",
3
+ "short_name": "ReelingIt",
4
+ "theme_color": "#43281C",
5
+ "display": "browser",
6
+ "background_color": "#56bce8",
7
+ "description": "The ultimate app for movie lovers: discover trailers, reviews, showtimes, and more. Experience cinema like never before!", "icons": [
8
+ {
9
+ "src": "images/icon.png",
10
+ "sizes": "1024x1024",
11
+ "type": "image/png"
12
+ }
13
+ ]
14
+ }
Binary file
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Central cache for dynamically imported route components.
3
+ * Stores already loaded modules to avoid repeated imports.
4
+ */
5
+ const routeCache = new Map();
6
+
7
+ /**
8
+ * Load a route component dynamically and cache it.
9
+ *
10
+ * @param {string} path - Unique path or key for the route module
11
+ * @param {() => Promise<any>} importer - Function that imports the module
12
+ * @returns {Promise<any>} - Resolves with the imported module
13
+ */
14
+ export async function loadRouteComponent(path, importer) {
15
+ if (routeCache.has(path)) {
16
+ return routeCache.get(path);
17
+ }
18
+
19
+ const module = await importer();
20
+ routeCache.set(path, module);
21
+ return module;
22
+ }
23
+
24
+ /**
25
+ * Prefetch a route component without rendering it.
26
+ *
27
+ * @param {string} path
28
+ * @param {() => Promise<any>} importer
29
+ */
30
+ export async function prefetchRouteComponent(path, importer) {
31
+ try {
32
+ await loadRouteComponent(path, importer);
33
+ } catch (e) {
34
+ console.error(`Prefetch failed for route ${path}:`, e);
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Check if a route component is already loaded.
40
+ *
41
+ * @param {string} path
42
+ * @returns {boolean}
43
+ */
44
+ export function isRouteLoaded(path) {
45
+ return routeCache.has(path);
46
+ }
47
+
48
+ /**
49
+ * Clear cache (optional, e.g., for HMR or logout scenarios)
50
+ */
51
+ export function clearRouteCache() {
52
+ routeCache.clear();
53
+ }
54
+
55
+ export default routeCache;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * HMR client script — injected only in dev mode (see root.html).
3
+ *
4
+ * Opens a Server-Sent Events connection to `/_vexjs/hmr`. When the server emits
5
+ * a `reload` event (triggered by a file change), the page reloads automatically
6
+ * so the developer always sees the latest version without a manual refresh.
7
+ *
8
+ * On error (e.g. server restart) the connection is closed silently — the
9
+ * browser will reconnect on the next page load.
10
+ */
11
+ (function () {
12
+ const evtSource = new EventSource("/_vexjs/hmr");
13
+
14
+ evtSource.addEventListener("reload", (e) => {
15
+ console.log(`[HMR] ${e.data || "file changed"} — reloading`);
16
+ location.reload();
17
+ });
18
+
19
+ evtSource.onerror = () => {
20
+ evtSource.close();
21
+ };
22
+ })();
@@ -0,0 +1,377 @@
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
+ }
@@ -0,0 +1,97 @@
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
+ if (document.readyState === "loading") {
83
+ document.addEventListener("DOMContentLoaded", () => {
84
+ hydrateComponents();
85
+ observer.disconnect();
86
+ });
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
+ })();
@@ -0,0 +1,25 @@
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
+ };
@@ -0,0 +1,9 @@
1
+ import { initializeRouter, navigate } from "./navigation/index.js";
2
+
3
+ window.app = {
4
+ navigate,
5
+ };
6
+
7
+ document.addEventListener("DOMContentLoaded", () => {
8
+ initializeRouter();
9
+ });