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.
Files changed (37) hide show
  1. package/compiler/parser/_extract.js +393 -0
  2. package/compiler/parser/blocks.js +361 -0
  3. package/compiler/parser/core.js +306 -0
  4. package/compiler/parser/expressions.js +386 -0
  5. package/compiler/parser/imports.js +108 -0
  6. package/compiler/parser/index.js +47 -0
  7. package/compiler/parser/state.js +155 -0
  8. package/compiler/parser/style.js +445 -0
  9. package/compiler/parser/view.js +632 -0
  10. package/compiler/parser.js +15 -2372
  11. package/compiler/parser.js.original +2376 -0
  12. package/package.json +2 -1
  13. package/runtime/a11y/announcements.js +213 -0
  14. package/runtime/a11y/contrast.js +125 -0
  15. package/runtime/a11y/focus.js +412 -0
  16. package/runtime/a11y/index.js +35 -0
  17. package/runtime/a11y/preferences.js +121 -0
  18. package/runtime/a11y/utils.js +164 -0
  19. package/runtime/a11y/validation.js +258 -0
  20. package/runtime/a11y/widgets.js +545 -0
  21. package/runtime/a11y.js +15 -1840
  22. package/runtime/a11y.js.original +1844 -0
  23. package/runtime/graphql/cache.js +69 -0
  24. package/runtime/graphql/client.js +563 -0
  25. package/runtime/graphql/hooks.js +492 -0
  26. package/runtime/graphql/index.js +62 -0
  27. package/runtime/graphql/subscriptions.js +241 -0
  28. package/runtime/graphql.js +12 -1322
  29. package/runtime/graphql.js.original +1326 -0
  30. package/runtime/router/core.js +956 -0
  31. package/runtime/router/guards.js +90 -0
  32. package/runtime/router/history.js +204 -0
  33. package/runtime/router/index.js +36 -0
  34. package/runtime/router/lazy.js +180 -0
  35. package/runtime/router/utils.js +226 -0
  36. package/runtime/router.js +12 -1600
  37. 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
+ }