pulse-js-framework 1.10.0 → 1.10.3
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/compiler/parser/_extract.js +393 -0
- package/compiler/parser/blocks.js +361 -0
- package/compiler/parser/core.js +306 -0
- package/compiler/parser/expressions.js +386 -0
- package/compiler/parser/imports.js +108 -0
- package/compiler/parser/index.js +47 -0
- package/compiler/parser/state.js +155 -0
- package/compiler/parser/style.js +445 -0
- package/compiler/parser/view.js +632 -0
- package/compiler/parser.js +15 -2372
- package/compiler/parser.js.original +2376 -0
- package/package.json +2 -1
- package/runtime/a11y/announcements.js +213 -0
- package/runtime/a11y/contrast.js +125 -0
- package/runtime/a11y/focus.js +412 -0
- package/runtime/a11y/index.js +35 -0
- package/runtime/a11y/preferences.js +121 -0
- package/runtime/a11y/utils.js +164 -0
- package/runtime/a11y/validation.js +258 -0
- package/runtime/a11y/widgets.js +545 -0
- package/runtime/a11y.js +15 -1840
- package/runtime/a11y.js.original +1844 -0
- package/runtime/graphql/cache.js +69 -0
- package/runtime/graphql/client.js +563 -0
- package/runtime/graphql/hooks.js +492 -0
- package/runtime/graphql/index.js +62 -0
- package/runtime/graphql/subscriptions.js +241 -0
- package/runtime/graphql.js +12 -1322
- package/runtime/graphql.js.original +1326 -0
- package/runtime/router/core.js +956 -0
- package/runtime/router/guards.js +90 -0
- package/runtime/router/history.js +204 -0
- package/runtime/router/index.js +36 -0
- package/runtime/router/lazy.js +180 -0
- package/runtime/router/utils.js +226 -0
- package/runtime/router.js +12 -1600
- package/runtime/router.js.original +1605 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse Router - Guards and Middleware
|
|
3
|
+
*
|
|
4
|
+
* Navigation guards and middleware system for route protection
|
|
5
|
+
*
|
|
6
|
+
* @module pulse-js-framework/runtime/router/guards
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { loggers } from '../logger.js';
|
|
10
|
+
|
|
11
|
+
const log = loggers.router;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Middleware context passed to each middleware function
|
|
15
|
+
* @typedef {Object} MiddlewareContext
|
|
16
|
+
* @property {NavigationTarget} to - Target route
|
|
17
|
+
* @property {NavigationTarget} from - Source route
|
|
18
|
+
* @property {Object} meta - Shared metadata between middlewares
|
|
19
|
+
* @property {function} redirect - Redirect to another path
|
|
20
|
+
* @property {function} abort - Abort navigation
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Create a middleware runner for the router
|
|
25
|
+
* Middlewares are executed in order, each can modify context or abort navigation
|
|
26
|
+
*
|
|
27
|
+
* @param {Array<function>} middlewares - Array of middleware functions
|
|
28
|
+
* @returns {function} Runner function
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* const authMiddleware = async (ctx, next) => {
|
|
32
|
+
* if (ctx.to.meta.requiresAuth && !isAuthenticated()) {
|
|
33
|
+
* return ctx.redirect('/login');
|
|
34
|
+
* }
|
|
35
|
+
* await next();
|
|
36
|
+
* };
|
|
37
|
+
*
|
|
38
|
+
* const loggerMiddleware = async (ctx, next) => {
|
|
39
|
+
* console.log('Navigating to:', ctx.to.path);
|
|
40
|
+
* const start = Date.now();
|
|
41
|
+
* await next();
|
|
42
|
+
* console.log('Navigation took:', Date.now() - start, 'ms');
|
|
43
|
+
* };
|
|
44
|
+
*
|
|
45
|
+
* const router = createRouter({
|
|
46
|
+
* routes,
|
|
47
|
+
* middleware: [loggerMiddleware, authMiddleware]
|
|
48
|
+
* });
|
|
49
|
+
*/
|
|
50
|
+
export function createMiddlewareRunner(middlewares) {
|
|
51
|
+
return async function runMiddleware(context) {
|
|
52
|
+
let index = 0;
|
|
53
|
+
let aborted = false;
|
|
54
|
+
let redirectPath = null;
|
|
55
|
+
|
|
56
|
+
// Create enhanced context with redirect and abort
|
|
57
|
+
const ctx = {
|
|
58
|
+
...context,
|
|
59
|
+
meta: {},
|
|
60
|
+
redirect: (path) => {
|
|
61
|
+
redirectPath = path;
|
|
62
|
+
},
|
|
63
|
+
abort: () => {
|
|
64
|
+
aborted = true;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
async function next() {
|
|
69
|
+
if (aborted || redirectPath) return;
|
|
70
|
+
if (index >= middlewares.length) return;
|
|
71
|
+
|
|
72
|
+
const middlewareIndex = index;
|
|
73
|
+
const middleware = middlewares[index++];
|
|
74
|
+
try {
|
|
75
|
+
await middleware(ctx, next);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
log.error(`Middleware error at index ${middlewareIndex}:`, error);
|
|
78
|
+
throw error; // Re-throw to halt navigation
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
await next();
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
aborted,
|
|
86
|
+
redirectPath,
|
|
87
|
+
meta: ctx.meta
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse Router - History and Scroll Management
|
|
3
|
+
*
|
|
4
|
+
* Browser history integration, scroll position restoration, and persistence
|
|
5
|
+
*
|
|
6
|
+
* @module pulse-js-framework/runtime/router/history
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { LRUCache } from '../lru-cache.js';
|
|
10
|
+
import { loggers } from '../logger.js';
|
|
11
|
+
|
|
12
|
+
const log = loggers.router;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Create scroll position manager with persistence
|
|
16
|
+
* @param {Object} options - Configuration
|
|
17
|
+
* @param {boolean} options.persist - Enable sessionStorage persistence
|
|
18
|
+
* @param {string} options.persistKey - Storage key name
|
|
19
|
+
* @returns {Object} Scroll position manager
|
|
20
|
+
*/
|
|
21
|
+
export function createScrollManager(options = {}) {
|
|
22
|
+
const { persist = false, persistKey = 'pulse-router-scroll' } = options;
|
|
23
|
+
|
|
24
|
+
// Scroll positions for history (LRU cache to prevent memory leaks)
|
|
25
|
+
// Keeps last 100 scroll positions - enough for typical navigation patterns
|
|
26
|
+
const scrollPositions = new LRUCache(100);
|
|
27
|
+
|
|
28
|
+
// Restore scroll positions from sessionStorage if persistence is enabled
|
|
29
|
+
if (persist && typeof sessionStorage !== 'undefined') {
|
|
30
|
+
try {
|
|
31
|
+
const stored = sessionStorage.getItem(persistKey);
|
|
32
|
+
if (stored) {
|
|
33
|
+
const parsed = JSON.parse(stored);
|
|
34
|
+
// Restore up to 100 most recent positions
|
|
35
|
+
const entries = Object.entries(parsed).slice(-100);
|
|
36
|
+
for (const [path, pos] of entries) {
|
|
37
|
+
if (pos && typeof pos.x === 'number' && typeof pos.y === 'number') {
|
|
38
|
+
scrollPositions.set(path, pos);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
log.debug(`Restored ${entries.length} scroll positions from sessionStorage`);
|
|
42
|
+
}
|
|
43
|
+
} catch (err) {
|
|
44
|
+
log.warn('Failed to restore scroll positions from sessionStorage:', err.message);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Persist scroll positions to sessionStorage
|
|
50
|
+
*/
|
|
51
|
+
function persistScrollPositions() {
|
|
52
|
+
if (!persist || typeof sessionStorage === 'undefined') return;
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const data = {};
|
|
56
|
+
for (const [path, pos] of scrollPositions.entries()) {
|
|
57
|
+
data[path] = pos;
|
|
58
|
+
}
|
|
59
|
+
sessionStorage.setItem(persistKey, JSON.stringify(data));
|
|
60
|
+
} catch (err) {
|
|
61
|
+
// SessionStorage may be full or disabled
|
|
62
|
+
log.warn('Failed to persist scroll positions:', err.message);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Save current scroll position for a path
|
|
68
|
+
*/
|
|
69
|
+
function saveScrollPosition(path) {
|
|
70
|
+
if (!path) return;
|
|
71
|
+
scrollPositions.set(path, {
|
|
72
|
+
x: window.scrollX,
|
|
73
|
+
y: window.scrollY
|
|
74
|
+
});
|
|
75
|
+
persistScrollPositions();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get saved scroll position for a path
|
|
80
|
+
*/
|
|
81
|
+
function getScrollPosition(path) {
|
|
82
|
+
return scrollPositions.get(path);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
saveScrollPosition,
|
|
87
|
+
getScrollPosition,
|
|
88
|
+
persistScrollPositions
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Handle scroll behavior after navigation
|
|
94
|
+
*/
|
|
95
|
+
export function handleScroll(to, from, savedPosition, scrollBehavior = null) {
|
|
96
|
+
if (scrollBehavior) {
|
|
97
|
+
let position;
|
|
98
|
+
try {
|
|
99
|
+
position = scrollBehavior(to, from, savedPosition);
|
|
100
|
+
} catch (err) {
|
|
101
|
+
log.warn(`scrollBehavior threw an error: ${err.message}`);
|
|
102
|
+
// Fall back to default behavior
|
|
103
|
+
window.scrollTo(0, 0);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Validate position is a valid object
|
|
108
|
+
if (position && typeof position === 'object') {
|
|
109
|
+
if (typeof position.selector === 'string' && position.selector) {
|
|
110
|
+
// Scroll to element
|
|
111
|
+
try {
|
|
112
|
+
const el = document.querySelector(position.selector);
|
|
113
|
+
if (el) {
|
|
114
|
+
const behavior = position.behavior === 'smooth' || position.behavior === 'auto'
|
|
115
|
+
? position.behavior
|
|
116
|
+
: 'auto';
|
|
117
|
+
el.scrollIntoView({ behavior });
|
|
118
|
+
}
|
|
119
|
+
} catch (err) {
|
|
120
|
+
log.warn(`Invalid selector in scrollBehavior: ${position.selector}`);
|
|
121
|
+
}
|
|
122
|
+
} else if (typeof position.x === 'number' || typeof position.y === 'number') {
|
|
123
|
+
const x = typeof position.x === 'number' && isFinite(position.x) ? position.x : 0;
|
|
124
|
+
const y = typeof position.y === 'number' && isFinite(position.y) ? position.y : 0;
|
|
125
|
+
const behavior = position.behavior === 'smooth' || position.behavior === 'auto'
|
|
126
|
+
? position.behavior
|
|
127
|
+
: 'auto';
|
|
128
|
+
window.scrollTo({ left: x, top: y, behavior });
|
|
129
|
+
}
|
|
130
|
+
// If position is object but no valid selector/x/y, do nothing (intentional no-scroll)
|
|
131
|
+
}
|
|
132
|
+
// If position is falsy (null/undefined/false), do nothing (intentional no-scroll)
|
|
133
|
+
} else if (savedPosition) {
|
|
134
|
+
// Default: restore saved position
|
|
135
|
+
const x = typeof savedPosition.x === 'number' && isFinite(savedPosition.x) ? savedPosition.x : 0;
|
|
136
|
+
const y = typeof savedPosition.y === 'number' && isFinite(savedPosition.y) ? savedPosition.y : 0;
|
|
137
|
+
window.scrollTo(x, y);
|
|
138
|
+
} else {
|
|
139
|
+
// Default: scroll to top
|
|
140
|
+
window.scrollTo(0, 0);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Save current scroll and wait for popstate to fire
|
|
146
|
+
* Used by back(), forward(), and go() to integrate with scroll restoration
|
|
147
|
+
* @returns {Promise} Resolves after popstate fires or timeout
|
|
148
|
+
*/
|
|
149
|
+
export function saveScrollAndWaitForPopState() {
|
|
150
|
+
// Return a Promise that resolves on the next popstate (with 100ms fallback)
|
|
151
|
+
return new Promise(resolve => {
|
|
152
|
+
let resolved = false;
|
|
153
|
+
const done = () => {
|
|
154
|
+
if (resolved) return;
|
|
155
|
+
resolved = true;
|
|
156
|
+
window.removeEventListener('popstate', listener);
|
|
157
|
+
resolve();
|
|
158
|
+
};
|
|
159
|
+
const listener = () => done();
|
|
160
|
+
window.addEventListener('popstate', listener);
|
|
161
|
+
setTimeout(done, 100);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Navigate back in browser history
|
|
167
|
+
* Saves scroll position before navigating
|
|
168
|
+
* @returns {Promise} Resolves after navigation completes
|
|
169
|
+
* @example
|
|
170
|
+
* await router.back(); // Go to previous page
|
|
171
|
+
*/
|
|
172
|
+
export function back() {
|
|
173
|
+
const promise = saveScrollAndWaitForPopState();
|
|
174
|
+
window.history.back();
|
|
175
|
+
return promise;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Navigate forward in browser history
|
|
180
|
+
* Saves scroll position before navigating
|
|
181
|
+
* @returns {Promise} Resolves after navigation completes
|
|
182
|
+
* @example
|
|
183
|
+
* await router.forward(); // Go to next page (if available)
|
|
184
|
+
*/
|
|
185
|
+
export function forward() {
|
|
186
|
+
const promise = saveScrollAndWaitForPopState();
|
|
187
|
+
window.history.forward();
|
|
188
|
+
return promise;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Navigate to a specific position in browser history
|
|
193
|
+
* Saves scroll position before navigating
|
|
194
|
+
* @param {number} delta - Number of entries to move (negative = back, positive = forward)
|
|
195
|
+
* @returns {Promise} Resolves after navigation completes
|
|
196
|
+
* @example
|
|
197
|
+
* await router.go(-2); // Go back 2 pages
|
|
198
|
+
* await router.go(1); // Go forward 1 page
|
|
199
|
+
*/
|
|
200
|
+
export function go(delta) {
|
|
201
|
+
const promise = saveScrollAndWaitForPopState();
|
|
202
|
+
window.history.go(delta);
|
|
203
|
+
return promise;
|
|
204
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse Router - Main Entry Point
|
|
3
|
+
*
|
|
4
|
+
* Barrel export for all router modules
|
|
5
|
+
*
|
|
6
|
+
* @module pulse-js-framework/runtime/router
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Export all from sub-modules
|
|
10
|
+
export * from './core.js';
|
|
11
|
+
export * from './lazy.js';
|
|
12
|
+
export * from './guards.js';
|
|
13
|
+
export * from './history.js';
|
|
14
|
+
export * from './utils.js';
|
|
15
|
+
|
|
16
|
+
// Default export for backward compatibility
|
|
17
|
+
import {
|
|
18
|
+
createRouter,
|
|
19
|
+
simpleRouter,
|
|
20
|
+
onBeforeLeave,
|
|
21
|
+
onAfterEnter
|
|
22
|
+
} from './core.js';
|
|
23
|
+
import { lazy, preload } from './lazy.js';
|
|
24
|
+
import { matchRoute, parseQuery, buildQueryString } from './utils.js';
|
|
25
|
+
|
|
26
|
+
export default {
|
|
27
|
+
createRouter,
|
|
28
|
+
simpleRouter,
|
|
29
|
+
lazy,
|
|
30
|
+
preload,
|
|
31
|
+
matchRoute,
|
|
32
|
+
parseQuery,
|
|
33
|
+
buildQueryString,
|
|
34
|
+
onBeforeLeave,
|
|
35
|
+
onAfterEnter
|
|
36
|
+
};
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse Router - Lazy Loading
|
|
3
|
+
*
|
|
4
|
+
* Lazy loading utilities for code-split route components
|
|
5
|
+
*
|
|
6
|
+
* @module pulse-js-framework/runtime/router/lazy
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { el } from '../dom.js';
|
|
10
|
+
import { loggers } from '../logger.js';
|
|
11
|
+
import { createVersionedAsync } from '../async.js';
|
|
12
|
+
import { Errors } from '../errors.js';
|
|
13
|
+
|
|
14
|
+
const log = loggers.router;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Lazy load helper for route components
|
|
18
|
+
* Wraps a dynamic import to provide loading states and error handling
|
|
19
|
+
*
|
|
20
|
+
* MEMORY SAFETY: Uses load version tracking to prevent stale promise callbacks
|
|
21
|
+
* from updating containers that are no longer in the DOM (e.g., after navigation).
|
|
22
|
+
*
|
|
23
|
+
* @param {function} importFn - Dynamic import function () => import('./Component.js')
|
|
24
|
+
* @param {Object} options - Lazy loading options
|
|
25
|
+
* @param {function} options.loading - Loading component function
|
|
26
|
+
* @param {function} options.error - Error component function
|
|
27
|
+
* @param {number} options.timeout - Timeout in ms (default: 10000)
|
|
28
|
+
* @param {number} options.delay - Delay before showing loading (default: 200)
|
|
29
|
+
* @returns {function} Lazy route handler
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* const routes = {
|
|
33
|
+
* '/dashboard': lazy(() => import('./Dashboard.js')),
|
|
34
|
+
* '/settings': lazy(() => import('./Settings.js'), {
|
|
35
|
+
* loading: () => el('div.spinner', 'Loading...'),
|
|
36
|
+
* error: (err) => el('div.error', `Failed to load: ${err.message}`),
|
|
37
|
+
* timeout: 5000
|
|
38
|
+
* })
|
|
39
|
+
* };
|
|
40
|
+
*/
|
|
41
|
+
export function lazy(importFn, options = {}) {
|
|
42
|
+
const {
|
|
43
|
+
loading: LoadingComponent = null,
|
|
44
|
+
error: ErrorComponent = null,
|
|
45
|
+
timeout = 10000,
|
|
46
|
+
delay = 200
|
|
47
|
+
} = options;
|
|
48
|
+
|
|
49
|
+
// Cache for loaded component
|
|
50
|
+
let cachedComponent = null;
|
|
51
|
+
let loadPromise = null;
|
|
52
|
+
|
|
53
|
+
// Use centralized versioned async for race condition handling
|
|
54
|
+
const versionController = createVersionedAsync();
|
|
55
|
+
|
|
56
|
+
return function lazyHandler(ctx) {
|
|
57
|
+
// Return cached component if already loaded
|
|
58
|
+
if (cachedComponent) {
|
|
59
|
+
return typeof cachedComponent === 'function'
|
|
60
|
+
? cachedComponent(ctx)
|
|
61
|
+
: cachedComponent.default
|
|
62
|
+
? cachedComponent.default(ctx)
|
|
63
|
+
: cachedComponent.render
|
|
64
|
+
? cachedComponent.render(ctx)
|
|
65
|
+
: cachedComponent;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Create container for async loading
|
|
69
|
+
const container = el('div.lazy-route');
|
|
70
|
+
|
|
71
|
+
// Start a new versioned load operation
|
|
72
|
+
const loadCtx = versionController.begin();
|
|
73
|
+
|
|
74
|
+
// Attach abort method to container for cleanup on navigation
|
|
75
|
+
container._pulseAbortLazyLoad = () => versionController.abort();
|
|
76
|
+
|
|
77
|
+
// Start loading if not already
|
|
78
|
+
if (!loadPromise) {
|
|
79
|
+
loadPromise = importFn();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Delay showing loading state to avoid flash (uses versioned timer)
|
|
83
|
+
if (LoadingComponent && delay > 0) {
|
|
84
|
+
loadCtx.setTimeout(() => {
|
|
85
|
+
if (!cachedComponent && loadCtx.isCurrent()) {
|
|
86
|
+
container.replaceChildren(LoadingComponent());
|
|
87
|
+
}
|
|
88
|
+
}, delay);
|
|
89
|
+
} else if (LoadingComponent) {
|
|
90
|
+
container.replaceChildren(LoadingComponent());
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Set timeout for loading (uses versioned timer)
|
|
94
|
+
let timeoutPromise = null;
|
|
95
|
+
if (timeout > 0) {
|
|
96
|
+
timeoutPromise = new Promise((_, reject) => {
|
|
97
|
+
loadCtx.setTimeout(() => {
|
|
98
|
+
reject(Errors.lazyTimeout(timeout));
|
|
99
|
+
}, timeout);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Race between load and timeout
|
|
104
|
+
const loadWithTimeout = timeoutPromise
|
|
105
|
+
? Promise.race([loadPromise, timeoutPromise])
|
|
106
|
+
: loadPromise;
|
|
107
|
+
|
|
108
|
+
loadWithTimeout
|
|
109
|
+
.then(module => {
|
|
110
|
+
// Always cache the component, even if navigation occurred
|
|
111
|
+
// This prevents re-showing loading state on future navigations
|
|
112
|
+
cachedComponent = module;
|
|
113
|
+
|
|
114
|
+
// Skip DOM updates if this load attempt is stale (navigation occurred)
|
|
115
|
+
if (loadCtx.isStale()) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Get the component from module
|
|
120
|
+
const Component = module.default || module;
|
|
121
|
+
const result = typeof Component === 'function'
|
|
122
|
+
? Component(ctx)
|
|
123
|
+
: Component.render
|
|
124
|
+
? Component.render(ctx)
|
|
125
|
+
: Component;
|
|
126
|
+
|
|
127
|
+
// Replace loading with actual component
|
|
128
|
+
loadCtx.ifCurrent(() => {
|
|
129
|
+
if (result instanceof Node) {
|
|
130
|
+
container.replaceChildren(result);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
})
|
|
134
|
+
.catch(err => {
|
|
135
|
+
loadPromise = null; // Allow retry
|
|
136
|
+
|
|
137
|
+
// Ignore if this load attempt is stale
|
|
138
|
+
if (loadCtx.isStale()) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (ErrorComponent) {
|
|
143
|
+
container.replaceChildren(ErrorComponent(err));
|
|
144
|
+
} else {
|
|
145
|
+
log.error('Lazy load error:', err);
|
|
146
|
+
container.replaceChildren(
|
|
147
|
+
el('div.lazy-error', `Failed to load component: ${err.message}`)
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
return container;
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Preload a lazy component without rendering
|
|
158
|
+
* Useful for prefetching on hover or when likely to navigate
|
|
159
|
+
*
|
|
160
|
+
* @param {function} lazyHandler - Lazy handler created with lazy()
|
|
161
|
+
* @returns {Promise} Resolves when component is loaded
|
|
162
|
+
*
|
|
163
|
+
* @example
|
|
164
|
+
* const DashboardLazy = lazy(() => import('./Dashboard.js'));
|
|
165
|
+
* // Preload on link hover
|
|
166
|
+
* link.addEventListener('mouseenter', () => preload(DashboardLazy));
|
|
167
|
+
*/
|
|
168
|
+
export function preload(lazyHandler) {
|
|
169
|
+
// Trigger the lazy handler with a dummy context to start loading
|
|
170
|
+
// The result is discarded, but the component will be cached
|
|
171
|
+
return new Promise(resolve => {
|
|
172
|
+
const result = lazyHandler({});
|
|
173
|
+
if (result instanceof Promise) {
|
|
174
|
+
result.then(resolve);
|
|
175
|
+
} else {
|
|
176
|
+
// Already loaded
|
|
177
|
+
resolve(result);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
}
|