loly 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.
package/dist/client.js ADDED
@@ -0,0 +1,727 @@
1
+ // src/client/router.ts
2
+ import { bootIslands, mount, activateAsyncComponents } from "loly-jsx";
3
+
4
+ // src/constants/server.ts
5
+ var SERVER = {
6
+ DEFAULT_PORT: 3e3,
7
+ RSC_ENDPOINT: "/__loly/rsc",
8
+ IMAGE_ENDPOINT: "/_loly/image",
9
+ ASYNC_ENDPOINT: "/__loly/async",
10
+ APP_CONTAINER_ID: "app"
11
+ };
12
+
13
+ // src/constants/errors.ts
14
+ var ERROR_MESSAGES = {
15
+ CONTEXT_NOT_INITIALIZED: "[loly-core] Framework context not initialized. Call setContext() first.",
16
+ APP_DIR_NOT_FOUND: (dir) => `[loly-core] src/app/ directory not found in ${dir}. Please ensure your project has a src/app/ directory.`,
17
+ ROUTE_NOT_FOUND: (pathname) => `Route not found: ${pathname}`,
18
+ PAGE_COMPONENT_NOT_FOUND: (path) => `[loly-core] Page component not found in ${path}`,
19
+ PAGE_COMPONENT_MUST_BE_FUNCTION: "[loly-core] Page component must be a function",
20
+ COMPONENT_NOT_FOUND: (route) => `Component not found for route: ${route}`,
21
+ APP_CONTAINER_NOT_FOUND: "[loly-core] App container not found (#app)",
22
+ CLIENT_BUILD_DIR_NOT_FOUND: (dir) => `[loly-core] Client build directory not found: ${dir}. Run 'loly build' first or the client scripts won't load.`,
23
+ ROUTES_MANIFEST_NOT_FOUND: (path) => `[loly-core] Routes manifest not found: ${path}. Run 'loly build' first.`,
24
+ DIRECTORY_NOT_FOUND: (dir) => `[loly-core] Directory not found: ${dir}`,
25
+ CLIENT_BUILD_FAILED: "Client build failed",
26
+ SERVER_BUILD_FAILED: "Server build failed",
27
+ FAILED_TO_LOAD_MODULE: (path) => `[loly-core] Failed to load module: ${path}`,
28
+ FAILED_TO_LOAD_ROUTE_COMPONENT: (path) => `[bootstrap] Failed to load component for route ${path}`,
29
+ FAILED_TO_LOAD_NESTED_LAYOUT: (path) => `[loly-core] Failed to load nested layout at ${path}`,
30
+ FAILED_TO_READ_CLIENT_MANIFEST: "[loly-core] Failed to read client manifest:",
31
+ FAILED_TO_PARSE_ISLAND_DATA: "[loly-core] Failed to parse island data:",
32
+ FATAL_BOOTSTRAP_ERROR: "[bootstrap] Fatal error during bootstrap:",
33
+ UNEXPECTED_ERROR: "[loly-core] Unexpected error:",
34
+ ERROR_STARTING_DEV_SERVER: "[loly-core] Error starting dev server:",
35
+ ERROR_STARTING_PROD_SERVER: "[loly-core] Error starting production server:",
36
+ ERROR_BUILDING_PROJECT: "[loly-core] Error building project:",
37
+ ERROR_HANDLING_REQUEST: "[loly-core] Error handling request:",
38
+ ERROR_RENDERING_PAGE: "[loly-core] Error rendering page:",
39
+ RSC_ENDPOINT_ERROR: "[loly-core] RSC endpoint error:"
40
+ };
41
+
42
+ // src/utils/path.ts
43
+ function normalizePath(path) {
44
+ if (path === "/") return "/";
45
+ return path.replace(/\/$/, "") || "/";
46
+ }
47
+ function normalizeUrlPath(pathname) {
48
+ return normalizePath(pathname);
49
+ }
50
+
51
+ // src/utils/route-matcher.ts
52
+ function matchRoutePattern(pattern, pathname) {
53
+ const normalizedPattern = normalizeUrlPath(pattern);
54
+ const normalizedPathname = normalizeUrlPath(pathname);
55
+ if (normalizedPattern === normalizedPathname) {
56
+ return { params: {} };
57
+ }
58
+ const patternParts = normalizedPattern.split("/").filter(Boolean);
59
+ const pathParts = normalizedPathname.split("/").filter(Boolean);
60
+ if (patternParts.length === 0 && pathParts.length === 0) {
61
+ return { params: {} };
62
+ }
63
+ const params = {};
64
+ let patternIdx = 0;
65
+ let pathIdx = 0;
66
+ while (patternIdx < patternParts.length && pathIdx < pathParts.length) {
67
+ const patternPart = patternParts[patternIdx];
68
+ const pathPart = pathParts[pathIdx];
69
+ if (patternPart.startsWith("*")) {
70
+ const paramName = patternPart.slice(1);
71
+ const remaining = pathParts.slice(pathIdx);
72
+ if (paramName) {
73
+ params[paramName] = remaining.join("/");
74
+ }
75
+ return { params };
76
+ }
77
+ if (patternPart.startsWith(":")) {
78
+ const paramName = patternPart.slice(1);
79
+ params[paramName] = decodeURIComponent(pathPart);
80
+ patternIdx++;
81
+ pathIdx++;
82
+ continue;
83
+ }
84
+ if (patternPart === pathPart) {
85
+ patternIdx++;
86
+ pathIdx++;
87
+ continue;
88
+ }
89
+ return null;
90
+ }
91
+ if (patternIdx === patternParts.length && pathIdx === pathParts.length) {
92
+ return { params };
93
+ }
94
+ return null;
95
+ }
96
+ function extractRouteParams(pattern, pathname) {
97
+ const match = matchRoutePattern(pattern, pathname);
98
+ return match?.params || {};
99
+ }
100
+ function matchesRoute(pattern, pathname) {
101
+ if (pattern === pathname) return true;
102
+ const normalizedPattern = normalizeUrlPath(pattern);
103
+ const normalizedPathname = normalizeUrlPath(pathname);
104
+ if (normalizedPattern === normalizedPathname) return true;
105
+ const escaped = normalizedPattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
106
+ const regex = new RegExp(
107
+ "^" + escaped.replace(/:[^/]+/g, "([^/]+)") + "/?$"
108
+ );
109
+ return regex.test(normalizedPathname);
110
+ }
111
+
112
+ // src/client/router.ts
113
+ var LRUCache = class {
114
+ constructor(maxSize = 10) {
115
+ this.maxSize = maxSize;
116
+ this.cache = /* @__PURE__ */ new Map();
117
+ }
118
+ get(key) {
119
+ if (!this.cache.has(key)) {
120
+ return void 0;
121
+ }
122
+ const value = this.cache.get(key);
123
+ this.cache.delete(key);
124
+ this.cache.set(key, value);
125
+ return value;
126
+ }
127
+ set(key, value) {
128
+ if (this.cache.has(key)) {
129
+ this.cache.delete(key);
130
+ } else if (this.cache.size >= this.maxSize) {
131
+ const firstKey = this.cache.keys().next().value;
132
+ if (firstKey !== void 0) {
133
+ this.cache.delete(firstKey);
134
+ }
135
+ }
136
+ this.cache.set(key, value);
137
+ }
138
+ has(key) {
139
+ return this.cache.has(key);
140
+ }
141
+ delete(key) {
142
+ return this.cache.delete(key);
143
+ }
144
+ clear() {
145
+ this.cache.clear();
146
+ }
147
+ get size() {
148
+ return this.cache.size;
149
+ }
150
+ };
151
+ function getInitialLocation() {
152
+ const { pathname, search, hash } = window.location;
153
+ return { pathname, search, hash };
154
+ }
155
+ function normalizePath2(path) {
156
+ const url = new URL(path, "http://localhost");
157
+ return {
158
+ pathname: normalizeUrlPath(url.pathname || "/"),
159
+ search: url.search,
160
+ hash: url.hash
161
+ };
162
+ }
163
+ async function fetchRSCPayload(pathname, search = "", options) {
164
+ const url = `${SERVER.RSC_ENDPOINT}?path=${encodeURIComponent(pathname)}${search ? `&search=${encodeURIComponent(search)}` : ""}`;
165
+ try {
166
+ const fetchOptions = {};
167
+ if (options?.priority) {
168
+ fetchOptions.priority = options.priority;
169
+ }
170
+ const response = await fetch(url, fetchOptions);
171
+ if (!response.ok) {
172
+ if (response.status === 404) return null;
173
+ throw new Error(`Failed to fetch RSC: ${response.statusText}`);
174
+ }
175
+ const rscPayload = await response.json();
176
+ return {
177
+ html: rscPayload.html,
178
+ scripts: rscPayload.scripts || "",
179
+ styles: rscPayload.styles || "",
180
+ islandData: rscPayload.islandData || {}
181
+ };
182
+ } catch (error) {
183
+ return null;
184
+ }
185
+ }
186
+ async function loadComponent(route) {
187
+ try {
188
+ const module = await route.component();
189
+ const Component = module.default || module;
190
+ if (!Component) {
191
+ throw new Error(ERROR_MESSAGES.COMPONENT_NOT_FOUND(route.path));
192
+ }
193
+ return (props) => {
194
+ const safeProps = props && typeof props === "object" && !Array.isArray(props) ? props : { params: {}, searchParams: {} };
195
+ if (typeof Component === "function") {
196
+ return Component(safeProps);
197
+ }
198
+ return Component;
199
+ };
200
+ } catch (error) {
201
+ throw error;
202
+ }
203
+ }
204
+ function bootIslandsNow() {
205
+ bootIslands();
206
+ }
207
+ var globalRouter = null;
208
+ function setGlobalRouter(router) {
209
+ globalRouter = router;
210
+ }
211
+ function getGlobalRouter() {
212
+ return globalRouter;
213
+ }
214
+ function navigate(to, options = {}) {
215
+ const router = getGlobalRouter();
216
+ if (router) {
217
+ router.navigate(to, options);
218
+ } else {
219
+ if (options.replace) {
220
+ window.history.replaceState({}, "", to);
221
+ } else {
222
+ window.history.pushState({}, "", to);
223
+ }
224
+ window.location.href = to;
225
+ }
226
+ }
227
+ var FrameworkRouter = class {
228
+ // Track links that have been pre-fetched
229
+ constructor(routes, appContainer) {
230
+ this.componentCache = /* @__PURE__ */ new Map();
231
+ this.loadedScripts = /* @__PURE__ */ new Set();
232
+ // Track loaded scripts
233
+ this.routeStyles = /* @__PURE__ */ new Map();
234
+ this.prefetchPromises = /* @__PURE__ */ new Map();
235
+ this.prefetchObserver = null;
236
+ this.MAX_CACHE_SIZE = 10;
237
+ this.invalidatedPaths = /* @__PURE__ */ new Set();
238
+ // Prepared for revalidatePath
239
+ this.prefetchedLinks = /* @__PURE__ */ new Set();
240
+ this.routes = routes;
241
+ this.appContainer = appContainer;
242
+ this.currentLocation = getInitialLocation();
243
+ this.rscPayloadCache = new LRUCache(this.MAX_CACHE_SIZE);
244
+ setGlobalRouter(this);
245
+ this.setupNavigation();
246
+ this.setupPopState();
247
+ this.setupPrefetch();
248
+ }
249
+ /**
250
+ * Check if pre-fetch should be performed based on connection
251
+ */
252
+ shouldPrefetch() {
253
+ const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
254
+ if (!connection) {
255
+ return true;
256
+ }
257
+ if (connection.effectiveType === "slow-2g" || connection.effectiveType === "2g") {
258
+ return false;
259
+ }
260
+ if (connection.saveData === true) {
261
+ return false;
262
+ }
263
+ return true;
264
+ }
265
+ /**
266
+ * Generate cache key from pathname and search
267
+ */
268
+ getCacheKey(pathname, search = "") {
269
+ return `${normalizeUrlPath(pathname)}${search || ""}`;
270
+ }
271
+ /**
272
+ * Check if href is an internal link
273
+ */
274
+ isInternalLink(href) {
275
+ if (!href) return false;
276
+ if (href.startsWith("http") || href.startsWith("//") || href.startsWith("mailto:") || href.startsWith("tel:")) {
277
+ return false;
278
+ }
279
+ return true;
280
+ }
281
+ /**
282
+ * Pre-fetch route data (RSC payload and JS component)
283
+ */
284
+ async prefetchRoute(pathname, search = "") {
285
+ if (!this.shouldPrefetch()) {
286
+ return null;
287
+ }
288
+ const key = this.getCacheKey(pathname, search);
289
+ if (this.invalidatedPaths.has(key)) {
290
+ return null;
291
+ }
292
+ const cached = this.rscPayloadCache.get(key);
293
+ if (cached) {
294
+ this.prefetchedLinks.add(key);
295
+ return cached;
296
+ }
297
+ if (this.prefetchPromises.has(key)) {
298
+ return this.prefetchPromises.get(key);
299
+ }
300
+ const promise = fetchRSCPayload(pathname, search, { priority: "low" }).then((payload) => {
301
+ if (payload) {
302
+ this.rscPayloadCache.set(key, payload);
303
+ this.prefetchedLinks.add(key);
304
+ const route = this.findRoute(pathname);
305
+ if (route) {
306
+ this.loadComponentCached(route).catch(() => {
307
+ });
308
+ }
309
+ }
310
+ return payload;
311
+ }).catch((error) => {
312
+ if (typeof console !== "undefined" && console.debug) {
313
+ console.debug("[loly-core] Pre-fetch failed:", error);
314
+ }
315
+ return null;
316
+ }).finally(() => {
317
+ this.prefetchPromises.delete(key);
318
+ });
319
+ this.prefetchPromises.set(key, promise);
320
+ return promise;
321
+ }
322
+ /**
323
+ * Navigate to a new path
324
+ */
325
+ navigate(to, options = {}) {
326
+ const next = normalizePath2(to);
327
+ if (options.replace) {
328
+ window.history.replaceState({}, "", to);
329
+ } else {
330
+ window.history.pushState({}, "", to);
331
+ }
332
+ this.currentLocation = next;
333
+ this.updateContent();
334
+ if (!next.hash) {
335
+ window.scrollTo({ top: 0 });
336
+ } else {
337
+ const el = document.getElementById(next.hash.slice(1));
338
+ if (el) {
339
+ el.scrollIntoView();
340
+ }
341
+ }
342
+ }
343
+ /**
344
+ * Update app content based on current location
345
+ */
346
+ async updateContent() {
347
+ const { pathname, search } = this.currentLocation;
348
+ const key = this.getCacheKey(pathname, search);
349
+ let rscPayload = this.rscPayloadCache.get(key) || null;
350
+ if (!rscPayload) {
351
+ rscPayload = await fetchRSCPayload(pathname, search);
352
+ if (rscPayload) {
353
+ this.rscPayloadCache.set(key, rscPayload);
354
+ }
355
+ }
356
+ this.prefetchPromises.delete(key);
357
+ if (rscPayload) {
358
+ const hasIslands = rscPayload.html.includes("data-loly-island");
359
+ if (hasIslands) {
360
+ const route2 = this.findRoute(pathname);
361
+ if (route2) {
362
+ try {
363
+ await this.loadComponentCached(route2);
364
+ } catch (err) {
365
+ console.warn(
366
+ "[loly-core] Failed to load route component for island registration:",
367
+ err
368
+ );
369
+ }
370
+ }
371
+ }
372
+ if (rscPayload.islandData && Object.keys(rscPayload.islandData).length > 0) {
373
+ if (!window.__LOLY_ISLAND_DATA__) {
374
+ window.__LOLY_ISLAND_DATA__ = {};
375
+ }
376
+ Object.assign(
377
+ window.__LOLY_ISLAND_DATA__,
378
+ rscPayload.islandData
379
+ );
380
+ }
381
+ const fragment = document.createDocumentFragment();
382
+ const tempDiv = document.createElement("div");
383
+ tempDiv.innerHTML = rscPayload.html;
384
+ while (tempDiv.firstChild) {
385
+ fragment.appendChild(tempDiv.firstChild);
386
+ }
387
+ this.appContainer.innerHTML = "";
388
+ this.appContainer.appendChild(fragment);
389
+ if (rscPayload.styles) {
390
+ this.injectStyles(pathname, rscPayload.styles);
391
+ }
392
+ if (rscPayload.scripts) {
393
+ await this.injectScripts(rscPayload.scripts);
394
+ }
395
+ requestAnimationFrame(() => {
396
+ requestAnimationFrame(() => {
397
+ bootIslandsNow();
398
+ this.observeNewLinks(this.appContainer);
399
+ });
400
+ });
401
+ return;
402
+ }
403
+ const route = this.findRoute(pathname);
404
+ if (route) {
405
+ try {
406
+ const Component = await this.loadComponentCached(route);
407
+ const searchParams = Object.fromEntries(
408
+ new URLSearchParams(search || "")
409
+ );
410
+ const params = extractRouteParams(route.path, pathname);
411
+ const result = Component({ params, searchParams });
412
+ this.appContainer.innerHTML = "";
413
+ mount(result, this.appContainer);
414
+ requestAnimationFrame(() => {
415
+ requestAnimationFrame(() => {
416
+ bootIslandsNow();
417
+ activateAsyncComponents(this.appContainer);
418
+ this.observeNewLinks(this.appContainer);
419
+ });
420
+ });
421
+ } catch (err) {
422
+ this.appContainer.innerHTML = "<div>Error loading route</div>";
423
+ }
424
+ } else {
425
+ this.appContainer.innerHTML = "<div>404 - Not Found</div>";
426
+ }
427
+ }
428
+ /**
429
+ * Inject styles for a route with deduplication
430
+ */
431
+ injectStyles(routePath, styles) {
432
+ const existing = this.routeStyles.get(routePath);
433
+ if (existing) {
434
+ existing.forEach((link) => {
435
+ if (!link.href.includes("globals.css")) {
436
+ link.remove();
437
+ }
438
+ });
439
+ }
440
+ if (!styles.trim()) return;
441
+ const tempDiv = document.createElement("div");
442
+ tempDiv.innerHTML = styles.trim();
443
+ const linkElements = [];
444
+ const links = tempDiv.querySelectorAll('link[rel="stylesheet"]');
445
+ links.forEach((link) => {
446
+ const href = link.getAttribute("href") || "";
447
+ if (href.includes("globals.css")) {
448
+ return;
449
+ }
450
+ const linkEl = document.createElement("link");
451
+ linkEl.rel = "stylesheet";
452
+ linkEl.href = href;
453
+ linkEl.setAttribute("data-route-styles", routePath);
454
+ const existingLink = document.querySelector(
455
+ `link[rel="stylesheet"][href="${href}"]`
456
+ );
457
+ if (!existingLink) {
458
+ document.head.appendChild(linkEl);
459
+ linkElements.push(linkEl);
460
+ }
461
+ });
462
+ if (linkElements.length > 0) {
463
+ this.routeStyles.set(routePath, linkElements);
464
+ }
465
+ }
466
+ /**
467
+ * Inject scripts with deduplication
468
+ */
469
+ async injectScripts(scripts) {
470
+ if (!scripts.trim()) return;
471
+ const tempDiv = document.createElement("div");
472
+ tempDiv.innerHTML = scripts;
473
+ const scriptTags = tempDiv.querySelectorAll("script");
474
+ const loadPromises = [];
475
+ scriptTags.forEach((script) => {
476
+ const src = script.getAttribute("src");
477
+ if (src) {
478
+ if (this.loadedScripts.has(src)) {
479
+ return;
480
+ }
481
+ this.loadedScripts.add(src);
482
+ const promise = new Promise((resolve, reject) => {
483
+ const newScript = document.createElement("script");
484
+ newScript.type = "module";
485
+ newScript.src = src;
486
+ newScript.onload = () => resolve();
487
+ newScript.onerror = () => {
488
+ this.loadedScripts.delete(src);
489
+ reject(new Error(`Failed to load script: ${src}`));
490
+ };
491
+ document.head.appendChild(newScript);
492
+ });
493
+ loadPromises.push(promise);
494
+ } else {
495
+ const inlineScript = document.createElement("script");
496
+ inlineScript.type = "module";
497
+ inlineScript.textContent = script.textContent || "";
498
+ document.head.appendChild(inlineScript);
499
+ setTimeout(() => inlineScript.remove(), 0);
500
+ }
501
+ });
502
+ await Promise.all(loadPromises);
503
+ }
504
+ /**
505
+ * Find matching route for pathname
506
+ */
507
+ findRoute(pathname) {
508
+ const exact = this.routes.find((r) => r.path === pathname);
509
+ if (exact) return exact;
510
+ for (const route of this.routes) {
511
+ if (matchesRoute(route.path, pathname)) {
512
+ return route;
513
+ }
514
+ }
515
+ return null;
516
+ }
517
+ /**
518
+ * Load component with caching
519
+ */
520
+ async loadComponentCached(route) {
521
+ if (this.componentCache.has(route.path)) {
522
+ return this.componentCache.get(route.path);
523
+ }
524
+ const Component = await loadComponent(route);
525
+ this.componentCache.set(route.path, Component);
526
+ return Component;
527
+ }
528
+ /**
529
+ * Setup navigation interception
530
+ */
531
+ setupNavigation() {
532
+ document.addEventListener("click", (e) => {
533
+ const target = e.target;
534
+ const link = target.closest("a");
535
+ if (!link) return;
536
+ const href = link.getAttribute("href");
537
+ if (!href) return;
538
+ if (href.startsWith("http") || href.startsWith("//") || href.startsWith("mailto:") || href.startsWith("tel:")) {
539
+ return;
540
+ }
541
+ if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey || e.button !== 0) {
542
+ return;
543
+ }
544
+ if (link.target && link.target !== "_self") {
545
+ return;
546
+ }
547
+ if (e.defaultPrevented) {
548
+ return;
549
+ }
550
+ e.preventDefault();
551
+ this.navigate(href);
552
+ });
553
+ }
554
+ /**
555
+ * Setup popstate handler for browser back/forward
556
+ */
557
+ setupPopState() {
558
+ window.addEventListener("popstate", () => {
559
+ this.currentLocation = getInitialLocation();
560
+ this.updateContent();
561
+ });
562
+ }
563
+ /**
564
+ * Observe new links in container (called after DOM updates)
565
+ */
566
+ observeNewLinks(container) {
567
+ if (!this.prefetchObserver) return;
568
+ const links = container.querySelectorAll("a[href]");
569
+ links.forEach((link) => {
570
+ const href = link.getAttribute("href");
571
+ if (href && this.isInternalLink(href)) {
572
+ const { pathname, search } = normalizePath2(href);
573
+ const key = this.getCacheKey(pathname, search);
574
+ if (!this.rscPayloadCache.has(key) && !this.prefetchedLinks.has(key)) {
575
+ this.prefetchObserver.observe(link);
576
+ }
577
+ }
578
+ });
579
+ }
580
+ /**
581
+ * Setup pre-fetch with Intersection Observer and hover
582
+ */
583
+ setupPrefetch() {
584
+ this.prefetchObserver = new IntersectionObserver(
585
+ (entries) => {
586
+ entries.forEach((entry) => {
587
+ if (entry.isIntersecting) {
588
+ const link = entry.target;
589
+ const href = link.getAttribute("href");
590
+ if (href && this.isInternalLink(href)) {
591
+ const { pathname, search } = normalizePath2(href);
592
+ const key = this.getCacheKey(pathname, search);
593
+ if (this.rscPayloadCache.has(key) || this.prefetchPromises.has(key)) {
594
+ this.prefetchObserver.unobserve(link);
595
+ this.prefetchedLinks.add(key);
596
+ return;
597
+ }
598
+ if (this.prefetchedLinks.has(key)) {
599
+ this.prefetchObserver.unobserve(link);
600
+ return;
601
+ }
602
+ this.prefetchedLinks.add(key);
603
+ this.prefetchRoute(pathname, search).then(() => {
604
+ this.prefetchObserver.unobserve(link);
605
+ });
606
+ }
607
+ }
608
+ });
609
+ },
610
+ { rootMargin: "200px" }
611
+ );
612
+ const allLinks = document.querySelectorAll("a[href]");
613
+ allLinks.forEach((link) => {
614
+ const href = link.getAttribute("href");
615
+ if (href && this.isInternalLink(href)) {
616
+ const { pathname, search } = normalizePath2(href);
617
+ const key = this.getCacheKey(pathname, search);
618
+ if (!this.rscPayloadCache.has(key) && !this.prefetchedLinks.has(key)) {
619
+ this.prefetchObserver.observe(link);
620
+ }
621
+ }
622
+ });
623
+ let hoverTimeout = null;
624
+ document.addEventListener(
625
+ "mouseenter",
626
+ (e) => {
627
+ const target = e.target;
628
+ if (!target || !(target instanceof Element)) {
629
+ return;
630
+ }
631
+ const link = target.closest("a");
632
+ if (link) {
633
+ const href = link.getAttribute("href");
634
+ if (href && this.isInternalLink(href)) {
635
+ const { pathname, search } = normalizePath2(href);
636
+ const key = this.getCacheKey(pathname, search);
637
+ if (this.rscPayloadCache.has(key) || this.prefetchedLinks.has(key)) {
638
+ return;
639
+ }
640
+ if (hoverTimeout) {
641
+ clearTimeout(hoverTimeout);
642
+ }
643
+ hoverTimeout = setTimeout(() => {
644
+ if (!this.rscPayloadCache.has(key) && !this.prefetchedLinks.has(key)) {
645
+ this.prefetchedLinks.add(key);
646
+ this.prefetchRoute(pathname, search);
647
+ }
648
+ }, 100);
649
+ }
650
+ }
651
+ },
652
+ true
653
+ // Capture phase
654
+ );
655
+ }
656
+ /**
657
+ * Initialize router (call after initial page load)
658
+ */
659
+ init() {
660
+ const self = this;
661
+ const initIslands = () => {
662
+ setTimeout(() => {
663
+ requestAnimationFrame(() => {
664
+ requestAnimationFrame(() => {
665
+ bootIslandsNow();
666
+ activateAsyncComponents(self.appContainer);
667
+ self.observeNewLinks(self.appContainer);
668
+ });
669
+ });
670
+ }, 0);
671
+ };
672
+ if (document.readyState === "loading") {
673
+ document.addEventListener("DOMContentLoaded", initIslands, { once: true });
674
+ } else {
675
+ initIslands();
676
+ }
677
+ }
678
+ /**
679
+ * Invalidate cache for a specific path (prepared for future revalidatePath implementation)
680
+ * TODO: Implementar invalidación completa cuando se implemente revalidatePath
681
+ */
682
+ revalidatePath(pathname, search = "") {
683
+ const key = this.getCacheKey(pathname, search);
684
+ this.invalidatedPaths.add(key);
685
+ this.rscPayloadCache.delete(key);
686
+ this.prefetchPromises.delete(key);
687
+ this.prefetchedLinks.delete(key);
688
+ }
689
+ };
690
+
691
+ // src/client/bootstrap.tsx
692
+ async function loadComponent2(route) {
693
+ try {
694
+ const module = await route.component();
695
+ const Component = module.default || module;
696
+ if (!Component) {
697
+ throw new Error(ERROR_MESSAGES.COMPONENT_NOT_FOUND(route.path));
698
+ }
699
+ return Component;
700
+ } catch (error) {
701
+ console.error(ERROR_MESSAGES.FAILED_TO_LOAD_ROUTE_COMPONENT(route.path), error);
702
+ throw error;
703
+ }
704
+ }
705
+ function bootstrapClient(options) {
706
+ const { routes } = options;
707
+ const appContainer = document.getElementById(SERVER.APP_CONTAINER_ID);
708
+ if (!appContainer) {
709
+ console.error(ERROR_MESSAGES.APP_CONTAINER_NOT_FOUND);
710
+ return;
711
+ }
712
+ const currentPath = window.location.pathname;
713
+ const currentRoute = routes.find((route) => matchesRoute(route.path, currentPath));
714
+ if (currentRoute) {
715
+ loadComponent2(currentRoute).then(() => {
716
+ }).catch(() => {
717
+ });
718
+ }
719
+ const router = new FrameworkRouter(routes, appContainer);
720
+ router.init();
721
+ }
722
+ export {
723
+ bootstrapClient,
724
+ getGlobalRouter,
725
+ navigate
726
+ };
727
+ //# sourceMappingURL=client.js.map