pulse-js-framework 1.10.4 → 1.11.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.
Files changed (65) hide show
  1. package/README.md +11 -0
  2. package/cli/build.js +13 -3
  3. package/compiler/directives.js +356 -0
  4. package/compiler/lexer.js +18 -3
  5. package/compiler/parser/core.js +6 -0
  6. package/compiler/parser/view.js +2 -6
  7. package/compiler/preprocessor.js +43 -23
  8. package/compiler/sourcemap.js +3 -1
  9. package/compiler/transformer/actions.js +329 -0
  10. package/compiler/transformer/export.js +7 -0
  11. package/compiler/transformer/expressions.js +85 -33
  12. package/compiler/transformer/imports.js +3 -0
  13. package/compiler/transformer/index.js +2 -0
  14. package/compiler/transformer/store.js +1 -1
  15. package/compiler/transformer/style.js +45 -16
  16. package/compiler/transformer/view.js +23 -2
  17. package/loader/rollup-plugin-server-components.js +391 -0
  18. package/loader/vite-plugin-server-components.js +420 -0
  19. package/loader/webpack-loader-server-components.js +356 -0
  20. package/package.json +124 -82
  21. package/runtime/async.js +4 -0
  22. package/runtime/context.js +16 -3
  23. package/runtime/dom-adapter.js +5 -3
  24. package/runtime/dom-virtual-list.js +2 -1
  25. package/runtime/form.js +8 -3
  26. package/runtime/graphql/cache.js +1 -1
  27. package/runtime/graphql/client.js +22 -0
  28. package/runtime/graphql/hooks.js +12 -6
  29. package/runtime/graphql/subscriptions.js +2 -0
  30. package/runtime/hmr.js +6 -3
  31. package/runtime/http.js +1 -0
  32. package/runtime/i18n.js +2 -0
  33. package/runtime/lru-cache.js +3 -1
  34. package/runtime/native.js +46 -20
  35. package/runtime/pulse.js +3 -0
  36. package/runtime/router/core.js +5 -1
  37. package/runtime/router/index.js +17 -1
  38. package/runtime/router/psc-integration.js +301 -0
  39. package/runtime/security.js +58 -29
  40. package/runtime/server-components/actions-server.js +798 -0
  41. package/runtime/server-components/actions.js +389 -0
  42. package/runtime/server-components/client.js +447 -0
  43. package/runtime/server-components/error-sanitizer.js +438 -0
  44. package/runtime/server-components/index.js +275 -0
  45. package/runtime/server-components/security-csrf.js +593 -0
  46. package/runtime/server-components/security-errors.js +227 -0
  47. package/runtime/server-components/security-ratelimit.js +733 -0
  48. package/runtime/server-components/security-validation.js +467 -0
  49. package/runtime/server-components/security.js +598 -0
  50. package/runtime/server-components/serializer.js +617 -0
  51. package/runtime/server-components/server.js +382 -0
  52. package/runtime/server-components/types.js +383 -0
  53. package/runtime/server-components/utils/mutex.js +60 -0
  54. package/runtime/server-components/utils/path-sanitizer.js +109 -0
  55. package/runtime/ssr.js +2 -1
  56. package/runtime/store.js +19 -10
  57. package/runtime/utils.js +12 -128
  58. package/types/animation.d.ts +300 -0
  59. package/types/i18n.d.ts +283 -0
  60. package/types/persistence.d.ts +267 -0
  61. package/types/sse.d.ts +248 -0
  62. package/types/sw.d.ts +150 -0
  63. package/runtime/a11y.js.original +0 -1844
  64. package/runtime/graphql.js.original +0 -1326
  65. package/runtime/router.js.original +0 -1605
@@ -0,0 +1,301 @@
1
+ /**
2
+ * PSC Integration for Router - Pulse Framework
3
+ *
4
+ * Handles Server Component navigation, caching, and prefetching.
5
+ * Provides client-side navigation with Server Component updates via PSC Wire Format.
6
+ *
7
+ * @module runtime/router/psc-integration
8
+ */
9
+
10
+ import { reconstructPSCTree } from '../server-components/index.js';
11
+
12
+ // ============================================================
13
+ // PSC Cache (LRU)
14
+ // ============================================================
15
+
16
+ /**
17
+ * LRU Cache for PSC payloads
18
+ * @class PSCCache
19
+ */
20
+ class PSCCache {
21
+ /**
22
+ * @param {number} maxSize - Maximum cache size
23
+ */
24
+ constructor(maxSize = 50) {
25
+ this.cache = new Map();
26
+ this.maxSize = maxSize;
27
+ }
28
+
29
+ /**
30
+ * Get cached entry (moves to most recent)
31
+ * @param {string} key - Cache key
32
+ * @returns {any|null} Cached value or null
33
+ */
34
+ get(key) {
35
+ if (!this.cache.has(key)) return null;
36
+
37
+ const entry = this.cache.get(key);
38
+ // Move to end (most recent)
39
+ this.cache.delete(key);
40
+ this.cache.set(key, entry);
41
+ return entry;
42
+ }
43
+
44
+ /**
45
+ * Set cache entry (evicts oldest if at capacity)
46
+ * @param {string} key - Cache key
47
+ * @param {any} value - Value to cache
48
+ */
49
+ set(key, value) {
50
+ // Delete if exists (will re-add at end)
51
+ if (this.cache.has(key)) {
52
+ this.cache.delete(key);
53
+ }
54
+
55
+ // Evict oldest if at capacity
56
+ if (this.cache.size >= this.maxSize) {
57
+ const firstKey = this.cache.keys().next().value;
58
+ this.cache.delete(firstKey);
59
+ }
60
+
61
+ this.cache.set(key, value);
62
+ }
63
+
64
+ /**
65
+ * Check if key exists in cache
66
+ * @param {string} key - Cache key
67
+ * @returns {boolean} True if key exists
68
+ */
69
+ has(key) {
70
+ return this.cache.has(key);
71
+ }
72
+
73
+ /**
74
+ * Clear all cache entries
75
+ */
76
+ clear() {
77
+ this.cache.clear();
78
+ }
79
+
80
+ /**
81
+ * Get cache size
82
+ * @returns {number} Number of cached entries
83
+ */
84
+ get size() {
85
+ return this.cache.size;
86
+ }
87
+ }
88
+
89
+ // ============================================================
90
+ // Cache Instance & Prefetch Queue
91
+ // ============================================================
92
+
93
+ /**
94
+ * Global PSC cache instance
95
+ * @type {PSCCache}
96
+ */
97
+ export const pscCache = new PSCCache();
98
+
99
+ /**
100
+ * Prefetch queue (tracks in-flight prefetches)
101
+ * @type {Set<string>}
102
+ */
103
+ const prefetchQueue = new Set();
104
+
105
+ // ============================================================
106
+ // PSC Fetch
107
+ // ============================================================
108
+
109
+ /**
110
+ * Fetch PSC payload from server
111
+ * @param {string} url - URL to fetch
112
+ * @param {Object} options - Fetch options
113
+ * @param {Object} [options.query={}] - Query parameters
114
+ * @param {AbortSignal} [options.signal] - Abort signal
115
+ * @returns {Promise<Object>} PSC payload
116
+ *
117
+ * @example
118
+ * const payload = await fetchPSCPayload('/products/123', {
119
+ * query: { tab: 'reviews' }
120
+ * });
121
+ */
122
+ export async function fetchPSCPayload(url, options = {}) {
123
+ const { query = {}, signal } = options;
124
+
125
+ // Build query string
126
+ const queryString = new URLSearchParams(query).toString();
127
+ const fullUrl = `${url}${queryString ? '?' + queryString : ''}`;
128
+
129
+ // Fetch from server with PSC headers
130
+ const response = await fetch(fullUrl, {
131
+ headers: {
132
+ 'Accept': 'application/x-pulse-psc',
133
+ 'X-Pulse-Request': 'navigation'
134
+ },
135
+ signal
136
+ });
137
+
138
+ if (!response.ok) {
139
+ throw new Error(`PSC fetch failed: ${response.status} ${response.statusText}`);
140
+ }
141
+
142
+ return response.json();
143
+ }
144
+
145
+ // ============================================================
146
+ // PSC Navigation
147
+ // ============================================================
148
+
149
+ /**
150
+ * Navigate to Server Component route
151
+ * @param {string} url - URL to navigate to
152
+ * @param {Object} options - Navigation options
153
+ * @param {Object} [options.query={}] - Query parameters
154
+ * @param {string} [options.cacheKey] - Cache key (auto-generated if not provided)
155
+ * @param {number} [options.staleTime=60000] - Cache staleness threshold (ms)
156
+ * @param {AbortSignal} [options.signal] - Abort signal
157
+ * @returns {Promise<Object>} PSC payload
158
+ *
159
+ * @example
160
+ * const payload = await navigatePSC('/products/123', {
161
+ * query: { tab: 'reviews' },
162
+ * staleTime: 30000
163
+ * });
164
+ */
165
+ export async function navigatePSC(url, options = {}) {
166
+ const { query = {}, cacheKey, signal } = options;
167
+ const staleTime = options.staleTime ?? 60000;
168
+
169
+ // Generate cache key if not provided
170
+ const key = cacheKey || (url + JSON.stringify(query));
171
+
172
+ // Check cache
173
+ if (pscCache.has(key)) {
174
+ const cached = pscCache.get(key);
175
+ const age = Date.now() - cached.timestamp;
176
+
177
+ if (age < staleTime) {
178
+ return cached.payload; // Fresh cache hit
179
+ }
180
+ }
181
+
182
+ // Fetch fresh payload
183
+ const payload = await fetchPSCPayload(url, { query, signal });
184
+
185
+ // Cache it
186
+ pscCache.set(key, {
187
+ payload,
188
+ timestamp: Date.now()
189
+ });
190
+
191
+ return payload;
192
+ }
193
+
194
+ // ============================================================
195
+ // PSC Prefetching
196
+ // ============================================================
197
+
198
+ /**
199
+ * Prefetch PSC payload in background
200
+ * @param {string} url - URL to prefetch
201
+ * @param {Object} options - Prefetch options
202
+ * @param {Object} [options.query={}] - Query parameters
203
+ * @param {number} [options.staleTime=60000] - Cache staleness threshold (ms)
204
+ * @returns {Promise<void>} Resolves when prefetch completes
205
+ *
206
+ * @example
207
+ * // Prefetch on link hover
208
+ * linkElement.addEventListener('mouseenter', () => {
209
+ * prefetchPSC('/products/123');
210
+ * });
211
+ */
212
+ export function prefetchPSC(url, options = {}) {
213
+ const { query = {} } = options;
214
+ const cacheKey = url + JSON.stringify(query);
215
+
216
+ // Skip if already prefetching or cached
217
+ if (prefetchQueue.has(cacheKey) || pscCache.has(cacheKey)) {
218
+ return Promise.resolve();
219
+ }
220
+
221
+ // Add to queue
222
+ prefetchQueue.add(cacheKey);
223
+
224
+ // Navigate (which fetches and caches)
225
+ return navigatePSC(url, { ...options, cacheKey })
226
+ .then(() => {
227
+ prefetchQueue.delete(cacheKey);
228
+ })
229
+ .catch((err) => {
230
+ console.warn('PSC prefetch failed:', err);
231
+ prefetchQueue.delete(cacheKey);
232
+ });
233
+ }
234
+
235
+ // ============================================================
236
+ // Cache Management
237
+ // ============================================================
238
+
239
+ /**
240
+ * Clear PSC cache
241
+ *
242
+ * @example
243
+ * clearPSCCache(); // Clear all cached PSC payloads
244
+ */
245
+ export function clearPSCCache() {
246
+ pscCache.clear();
247
+ }
248
+
249
+ /**
250
+ * Get cache statistics
251
+ * @returns {Object} Cache stats
252
+ *
253
+ * @example
254
+ * const stats = getPSCCacheStats();
255
+ * console.log(`Cache: ${stats.size}/${stats.maxSize} entries`);
256
+ */
257
+ export function getPSCCacheStats() {
258
+ return {
259
+ size: pscCache.size,
260
+ maxSize: pscCache.maxSize,
261
+ prefetching: prefetchQueue.size
262
+ };
263
+ }
264
+
265
+ /**
266
+ * Configure PSC cache
267
+ * @param {Object} options - Configuration options
268
+ * @param {number} [options.maxSize] - Maximum cache size
269
+ *
270
+ * @example
271
+ * configurePSCCache({ maxSize: 100 }); // Increase cache size
272
+ */
273
+ export function configurePSCCache(options = {}) {
274
+ if (options.maxSize !== undefined) {
275
+ // Create new cache with new max size
276
+ const newCache = new PSCCache(options.maxSize);
277
+
278
+ // Copy existing entries (up to new max size)
279
+ const entries = Array.from(pscCache.cache.entries()).slice(0, options.maxSize);
280
+ entries.forEach(([key, value]) => newCache.set(key, value));
281
+
282
+ // Replace global cache
283
+ pscCache.cache = newCache.cache;
284
+ pscCache.maxSize = newCache.maxSize;
285
+ }
286
+ }
287
+
288
+ // ============================================================
289
+ // Exports
290
+ // ============================================================
291
+
292
+ export default {
293
+ PSCCache,
294
+ pscCache,
295
+ fetchPSCPayload,
296
+ navigatePSC,
297
+ prefetchPSC,
298
+ clearPSCCache,
299
+ getPSCCacheStats,
300
+ configurePSCCache
301
+ };
@@ -166,10 +166,11 @@ export function sanitizeObjectKeys(obj, options = {}) {
166
166
  // =============================================================================
167
167
 
168
168
  /**
169
- * HTML entity escape map
170
- * @private
169
+ * HTML entity escape map.
170
+ * Single source of truth — imported by utils.js and error-sanitizer.js.
171
+ * @type {Object<string, string>}
171
172
  */
172
- const HTML_ESCAPES = {
173
+ export const HTML_ESCAPES = {
173
174
  '&': '&amp;',
174
175
  '<': '&lt;',
175
176
  '>': '&gt;',
@@ -177,10 +178,17 @@ const HTML_ESCAPES = {
177
178
  "'": '&#39;'
178
179
  };
179
180
 
181
+ /**
182
+ * Pre-compiled regex for HTML special characters (generated from HTML_ESCAPES keys).
183
+ * @private
184
+ */
185
+ const HTML_ESCAPE_REGEX = new RegExp(`[${Object.keys(HTML_ESCAPES).join('')}]`, 'g');
186
+
180
187
  /**
181
188
  * Escape HTML special characters to prevent XSS.
189
+ * Single source of truth — re-exported by utils.js.
182
190
  *
183
- * @param {string} str - String to escape
191
+ * @param {*} str - Value to escape (will be converted to string)
184
192
  * @returns {string} Escaped string safe for HTML insertion
185
193
  *
186
194
  * @example
@@ -189,7 +197,7 @@ const HTML_ESCAPES = {
189
197
  */
190
198
  export function escapeHtml(str) {
191
199
  if (str == null) return '';
192
- return String(str).replace(/[&<>"']/g, char => HTML_ESCAPES[char]);
200
+ return String(str).replace(HTML_ESCAPE_REGEX, char => HTML_ESCAPES[char]);
193
201
  }
194
202
 
195
203
  /**
@@ -398,38 +406,36 @@ export function sanitizeUrl(url, options = {}) {
398
406
 
399
407
  const trimmed = String(url).trim();
400
408
 
401
- // Decode URL to catch encoded attacks
409
+ // Decode URL to catch encoded attacks like &#x6a;avascript:
410
+ // Also handles %6A%61%76%61%73%63%72%69%70%74 encoding
402
411
  let decoded = trimmed;
403
412
  try {
404
- // Decode HTML entities first
413
+ // Decode HTML entities first (&#x6a; -> j)
405
414
  decoded = decoded.replace(/&#x([0-9a-f]+);?/gi, (_, hex) =>
406
415
  String.fromCharCode(parseInt(hex, 16))
407
416
  );
408
417
  decoded = decoded.replace(/&#(\d+);?/g, (_, dec) =>
409
418
  String.fromCharCode(parseInt(dec, 10))
410
419
  );
411
- // Then decode URI encoding
420
+ // Then decode URI encoding (%6A -> j)
412
421
  decoded = decodeURIComponent(decoded);
413
422
  } catch {
414
- // Malformed URL - use original
423
+ // If decoding fails, use original (malformed URLs will be blocked anyway)
415
424
  }
416
425
 
417
- // Normalize and check protocol
426
+ // Normalize: lowercase and remove whitespace for protocol check
418
427
  const normalized = decoded.toLowerCase().replace(/[\s\x00-\x1f]/g, '');
419
428
 
420
- // Block javascript: protocol
421
- if (normalized.startsWith('javascript:')) {
422
- log.warn('Blocked javascript: URL');
423
- return null;
424
- }
425
-
426
- // Block vbscript: protocol
427
- if (normalized.startsWith('vbscript:')) {
428
- log.warn('Blocked vbscript: URL');
429
- return null;
429
+ // Block dangerous protocols
430
+ const dangerousProtocols = ['javascript:', 'vbscript:', 'file:'];
431
+ for (const protocol of dangerousProtocols) {
432
+ if (normalized.startsWith(protocol)) {
433
+ log.warn(`Blocked ${protocol} URL`);
434
+ return null;
435
+ }
430
436
  }
431
437
 
432
- // Check data: URLs
438
+ // Check for data: protocol
433
439
  if (normalized.startsWith('data:')) {
434
440
  if (!allowData) {
435
441
  log.warn('Blocked data: URL (not allowed)');
@@ -440,20 +446,42 @@ export function sanitizeUrl(url, options = {}) {
440
446
  log.warn('Blocked dangerous data: URL');
441
447
  return null;
442
448
  }
449
+ return trimmed;
443
450
  }
444
451
 
445
- // Check blob: URLs
446
- if (normalized.startsWith('blob:') && !allowBlob) {
447
- log.warn('Blocked blob: URL (not allowed)');
448
- return null;
452
+ // Check for blob: protocol
453
+ if (normalized.startsWith('blob:')) {
454
+ if (!allowBlob) {
455
+ log.warn('Blocked blob: URL (not allowed)');
456
+ return null;
457
+ }
458
+ return trimmed;
459
+ }
460
+
461
+ // Allow relative URLs (must start with / or . to prevent //evil.com attacks)
462
+ if (allowRelative) {
463
+ if (trimmed.startsWith('/') && !trimmed.startsWith('//')) {
464
+ return trimmed;
465
+ }
466
+ if (trimmed.startsWith('./') || trimmed.startsWith('../')) {
467
+ return trimmed;
468
+ }
469
+ // URLs without protocol that don't start with // are relative
470
+ if (!trimmed.includes(':') && !trimmed.startsWith('//')) {
471
+ return trimmed;
472
+ }
449
473
  }
450
474
 
451
- // Check for relative URLs
452
- if (!trimmed.includes(':')) {
453
- return allowRelative ? trimmed : null;
475
+ // Allow safe protocols (http, https, mailto, tel, sms, ftp, sftp)
476
+ const colonIndex = trimmed.indexOf(':');
477
+ if (colonIndex > 0) {
478
+ const protocol = trimmed.slice(0, colonIndex + 1).toLowerCase();
479
+ if (SAFE_PROTOCOLS.has(protocol)) {
480
+ return trimmed;
481
+ }
454
482
  }
455
483
 
456
- return trimmed;
484
+ return null;
457
485
  }
458
486
 
459
487
  // =============================================================================
@@ -468,6 +496,7 @@ export default {
468
496
  SAFE_PROTOCOLS,
469
497
  DEFAULT_ALLOWED_TAGS,
470
498
  DEFAULT_ALLOWED_ATTRS,
499
+ HTML_ESCAPES,
471
500
 
472
501
  // Validation
473
502
  isDangerousKey,