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
package/runtime/utils.js CHANGED
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import { createLogger } from './logger.js';
9
- import { sanitizeHtml } from './security.js';
9
+ import { sanitizeHtml, DANGEROUS_KEYS, escapeHtml, sanitizeUrl } from './security.js';
10
10
 
11
11
  const log = createLogger('Security');
12
12
 
@@ -14,43 +14,9 @@ const log = createLogger('Security');
14
14
  // XSS Prevention
15
15
  // ============================================================================
16
16
 
17
- /**
18
- * HTML entity escape map
19
- * @private
20
- */
21
- const HTML_ESCAPES = {
22
- '&': '&',
23
- '<': '&lt;',
24
- '>': '&gt;',
25
- '"': '&quot;',
26
- "'": '&#39;'
27
- };
28
-
29
- /**
30
- * Regex for HTML special characters (auto-generated from HTML_ESCAPES keys)
31
- * @private
32
- */
33
- const HTML_ESCAPE_REGEX = new RegExp(`[${Object.keys(HTML_ESCAPES).join('')}]`, 'g');
34
-
35
- /**
36
- * Escape HTML special characters to prevent XSS attacks.
37
- * Use this when inserting untrusted content into HTML.
38
- *
39
- * @param {*} str - Value to escape (will be converted to string)
40
- * @returns {string} Escaped string safe for HTML insertion
41
- *
42
- * @example
43
- * escapeHtml('<script>alert("xss")</script>')
44
- * // Returns: '&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;'
45
- *
46
- * @example
47
- * // Safe to insert into HTML
48
- * element.innerHTML = `<div>${escapeHtml(userInput)}</div>`;
49
- */
50
- export function escapeHtml(str) {
51
- if (str == null) return '';
52
- return String(str).replace(HTML_ESCAPE_REGEX, char => HTML_ESCAPES[char]);
53
- }
17
+ // escapeHtml and sanitizeUrl are imported from security.js (single source of truth)
18
+ // and re-exported below for backward compatibility.
19
+ export { escapeHtml, sanitizeUrl };
54
20
 
55
21
  /**
56
22
  * Unescape HTML entities back to their original characters.
@@ -101,6 +67,12 @@ export function dangerouslySetInnerHTML(element, html, options = {}) {
101
67
  if (sanitize) {
102
68
  element.innerHTML = sanitizeHtml(html, sanitizeOptions);
103
69
  } else {
70
+ if (typeof process === 'undefined' || process.env?.NODE_ENV !== 'production') {
71
+ log.warn(
72
+ 'dangerouslySetInnerHTML() called without sanitization. ' +
73
+ 'Pass { sanitize: true } to enable HTML sanitization and prevent XSS.'
74
+ );
75
+ }
104
76
  element.innerHTML = html;
105
77
  }
106
78
  }
@@ -249,95 +221,7 @@ export function safeSetAttribute(element, name, value, options = {}, domAdapter
249
221
  return true;
250
222
  }
251
223
 
252
- // ============================================================================
253
- // URL Validation
254
- // ============================================================================
255
-
256
- /**
257
- * Validate and sanitize a URL to prevent javascript: and data: XSS.
258
- *
259
- * Security protections:
260
- * - Blocks javascript: URLs (including encoded variants)
261
- * - Blocks data: URLs by default (can be enabled)
262
- * - Blocks blob: URLs by default (can be enabled)
263
- * - Blocks vbscript: URLs
264
- * - Decodes URL before checking to prevent encoding bypass
265
- *
266
- * @param {string} url - URL to validate
267
- * @param {Object} [options] - Validation options
268
- * @param {boolean} [options.allowData=false] - Allow data: URLs
269
- * @param {boolean} [options.allowBlob=false] - Allow blob: URLs
270
- * @param {boolean} [options.allowRelative=true] - Allow relative URLs
271
- * @returns {string|null} Sanitized URL or null if invalid
272
- *
273
- * @example
274
- * const safeUrl = sanitizeUrl(userProvidedUrl);
275
- * if (safeUrl) {
276
- * link.href = safeUrl;
277
- * }
278
- */
279
- export function sanitizeUrl(url, options = {}) {
280
- const { allowData = false, allowBlob = false, allowRelative = true } = options;
281
-
282
- if (url == null || url === '') return null;
283
-
284
- const trimmed = String(url).trim();
285
-
286
- // Decode URL to catch encoded attacks like &#x6a;avascript:
287
- // Also handles %6A%61%76%61%73%63%72%69%70%74 encoding
288
- let decoded = trimmed;
289
- try {
290
- // Decode HTML entities first (&#x6a; -> j)
291
- decoded = decoded.replace(/&#x([0-9a-f]+);?/gi, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
292
- decoded = decoded.replace(/&#(\d+);?/g, (_, dec) => String.fromCharCode(parseInt(dec, 10)));
293
- // Then decode URI encoding (%6A -> j)
294
- decoded = decodeURIComponent(decoded);
295
- } catch {
296
- // If decoding fails, use original (malformed URLs will be blocked anyway)
297
- }
298
-
299
- // Normalize: lowercase and remove whitespace for protocol check
300
- const normalized = decoded.toLowerCase().replace(/[\s\x00-\x1f]/g, '');
301
-
302
- // Block dangerous protocols
303
- const dangerousProtocols = ['javascript:', 'vbscript:', 'file:'];
304
- for (const protocol of dangerousProtocols) {
305
- if (normalized.startsWith(protocol)) {
306
- return null;
307
- }
308
- }
309
-
310
- // Check for data: protocol
311
- if (normalized.startsWith('data:')) {
312
- return allowData ? trimmed : null;
313
- }
314
-
315
- // Check for blob: protocol
316
- if (normalized.startsWith('blob:')) {
317
- return allowBlob ? trimmed : null;
318
- }
319
-
320
- // Allow relative URLs (must start with / or . to prevent //evil.com attacks)
321
- if (allowRelative) {
322
- if (trimmed.startsWith('/') && !trimmed.startsWith('//')) {
323
- return trimmed;
324
- }
325
- if (trimmed.startsWith('./') || trimmed.startsWith('../')) {
326
- return trimmed;
327
- }
328
- // URLs without protocol that don't start with // are relative
329
- if (!trimmed.includes(':') && !trimmed.startsWith('//')) {
330
- return trimmed;
331
- }
332
- }
333
-
334
- // Only allow http: and https: protocols
335
- if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
336
- return trimmed;
337
- }
338
-
339
- return null;
340
- }
224
+ // sanitizeUrl is imported from security.js and re-exported at the top of this file.
341
225
 
342
226
  // ============================================================================
343
227
  // CSS Sanitization
@@ -515,7 +399,7 @@ export function deepClone(obj) {
515
399
 
516
400
  const clone = {};
517
401
  for (const key in obj) {
518
- if (Object.prototype.hasOwnProperty.call(obj, key)) {
402
+ if (Object.prototype.hasOwnProperty.call(obj, key) && !DANGEROUS_KEYS.has(key)) {
519
403
  clone[key] = deepClone(obj[key]);
520
404
  }
521
405
  }
@@ -0,0 +1,300 @@
1
+ /**
2
+ * Pulse Animation Module Type Definitions
3
+ * @module pulse-js-framework/runtime/animation
4
+ */
5
+
6
+ import { Pulse } from './pulse';
7
+
8
+ // ============================================================================
9
+ // Configuration
10
+ // ============================================================================
11
+
12
+ /** Global animation configuration options */
13
+ export interface AnimationConfig {
14
+ /** Respect the user's prefers-reduced-motion setting (default: true) */
15
+ respectReducedMotion?: boolean;
16
+
17
+ /** Default animation duration in ms (default: 300) */
18
+ defaultDuration?: number;
19
+
20
+ /** Default easing function (default: 'ease-out') */
21
+ defaultEasing?: string;
22
+
23
+ /** Kill switch to disable all animations globally (default: false) */
24
+ disabled?: boolean;
25
+ }
26
+
27
+ /**
28
+ * Configure global animation settings.
29
+ *
30
+ * @param options Configuration options
31
+ *
32
+ * @example
33
+ * configureAnimations({
34
+ * respectReducedMotion: true,
35
+ * defaultDuration: 200,
36
+ * defaultEasing: 'ease-in-out',
37
+ * disabled: false,
38
+ * });
39
+ */
40
+ export declare function configureAnimations(options?: AnimationConfig): void;
41
+
42
+ // ============================================================================
43
+ // animate()
44
+ // ============================================================================
45
+
46
+ /** Options for the animate() function */
47
+ export interface AnimateOptions {
48
+ /** Duration in ms (default: global defaultDuration) */
49
+ duration?: number;
50
+
51
+ /** Easing function (default: global defaultEasing) */
52
+ easing?: string;
53
+
54
+ /** Fill mode (default: 'none') */
55
+ fill?: FillMode;
56
+
57
+ /** Delay in ms before animation starts (default: 0) */
58
+ delay?: number;
59
+
60
+ /** Number of iterations (default: 1) */
61
+ iterations?: number;
62
+
63
+ /** Playback direction (default: 'normal') */
64
+ direction?: PlaybackDirection;
65
+ }
66
+
67
+ /** Animation control returned by animate() */
68
+ export interface AnimationControl {
69
+ /** Reactive playing state */
70
+ isPlaying: Pulse<boolean>;
71
+
72
+ /** Reactive progress (0 to 1) */
73
+ progress: Pulse<number>;
74
+
75
+ /** Promise that resolves when the animation finishes */
76
+ finished: Promise<void>;
77
+
78
+ /** Resume or start playback */
79
+ play(): void;
80
+
81
+ /** Pause the animation */
82
+ pause(): void;
83
+
84
+ /** Reverse the animation direction */
85
+ reverse(): void;
86
+
87
+ /** Cancel the animation and reset progress to 0 */
88
+ cancel(): void;
89
+
90
+ /** Finish the animation immediately (progress set to 1) */
91
+ finish(): void;
92
+
93
+ /** Dispose the animation and clean up resources */
94
+ dispose(): void;
95
+ }
96
+
97
+ /**
98
+ * Animate an element using the Web Animations API.
99
+ * Automatically respects reduced motion preferences and SSR environments.
100
+ * Returns a no-op control when animations are disabled.
101
+ *
102
+ * @param element Element to animate
103
+ * @param keyframes Keyframes in WAAPI format (array of keyframe objects or a single keyframe object)
104
+ * @param options Animation options
105
+ * @returns AnimationControl with reactive state and playback methods
106
+ *
107
+ * @example
108
+ * const ctrl = animate(element, [
109
+ * { opacity: 0, transform: 'translateY(-10px)' },
110
+ * { opacity: 1, transform: 'translateY(0)' },
111
+ * ], { duration: 300, easing: 'ease-out' });
112
+ *
113
+ * effect(() => {
114
+ * console.log('Progress:', ctrl.progress.get());
115
+ * });
116
+ *
117
+ * await ctrl.finished;
118
+ */
119
+ export declare function animate(
120
+ element: HTMLElement,
121
+ keyframes: Keyframe[] | PropertyIndexedKeyframes,
122
+ options?: AnimateOptions
123
+ ): AnimationControl;
124
+
125
+ // ============================================================================
126
+ // useTransition()
127
+ // ============================================================================
128
+
129
+ /** Options for the useTransition() hook */
130
+ export interface TransitionOptions {
131
+ /** Enter animation keyframes (default: { opacity: [0, 1] }) */
132
+ enter?: Keyframe[] | PropertyIndexedKeyframes;
133
+
134
+ /** Leave animation keyframes (default: { opacity: [1, 0] }) */
135
+ leave?: Keyframe[] | PropertyIndexedKeyframes;
136
+
137
+ /** Animation duration in ms */
138
+ duration?: number;
139
+
140
+ /** Easing function */
141
+ easing?: string;
142
+
143
+ /** Template factory called when entering (condition becomes true) */
144
+ onEnter?: (() => Node | Node[] | null) | null;
145
+
146
+ /** Template factory called when leaving (condition becomes false) */
147
+ onLeave?: (() => Node | Node[] | null) | null;
148
+ }
149
+
150
+ /** Return type of useTransition() */
151
+ export interface TransitionResult {
152
+ /** Document fragment container for the transition content */
153
+ container: DocumentFragment;
154
+
155
+ /** Reactive flag: true while enter animation is playing */
156
+ isEntering: Pulse<boolean>;
157
+
158
+ /** Reactive flag: true while leave animation is playing */
159
+ isLeaving: Pulse<boolean>;
160
+ }
161
+
162
+ /**
163
+ * Reactive enter/leave transition hook.
164
+ * Animates content in and out based on a reactive condition.
165
+ * Automatically cleans up when the enclosing effect is disposed.
166
+ *
167
+ * @param condition Reactive condition function (or static boolean)
168
+ * @param options Transition configuration
169
+ * @returns Transition result with container and reactive state
170
+ *
171
+ * @example
172
+ * const { container, isEntering, isLeaving } = useTransition(
173
+ * () => showModal.get(),
174
+ * {
175
+ * enter: { opacity: [0, 1], transform: ['scale(0.9)', 'scale(1)'] },
176
+ * leave: { opacity: [1, 0], transform: ['scale(1)', 'scale(0.9)'] },
177
+ * duration: 200,
178
+ * onEnter: () => el('.modal', 'Hello!'),
179
+ * }
180
+ * );
181
+ */
182
+ export declare function useTransition(
183
+ condition: (() => boolean) | boolean,
184
+ options?: TransitionOptions
185
+ ): TransitionResult;
186
+
187
+ // ============================================================================
188
+ // useSpring()
189
+ // ============================================================================
190
+
191
+ /** Options for the useSpring() hook */
192
+ export interface SpringOptions {
193
+ /** Spring stiffness (default: 170) */
194
+ stiffness?: number;
195
+
196
+ /** Damping coefficient (default: 26) */
197
+ damping?: number;
198
+
199
+ /** Mass (default: 1) */
200
+ mass?: number;
201
+
202
+ /** Precision threshold for settling (default: 0.01) */
203
+ precision?: number;
204
+ }
205
+
206
+ /** Return type of useSpring() */
207
+ export interface SpringResult {
208
+ /** Reactive current value of the spring */
209
+ value: Pulse<number>;
210
+
211
+ /** Reactive flag: true while the spring is animating */
212
+ isAnimating: Pulse<boolean>;
213
+
214
+ /**
215
+ * Set a new target value for the spring to animate towards
216
+ * @param newTarget New target value
217
+ */
218
+ set(newTarget: number): void;
219
+
220
+ /** Dispose the spring animation and clean up resources */
221
+ dispose(): void;
222
+ }
223
+
224
+ /**
225
+ * Spring-based animation using a damped harmonic oscillator.
226
+ * Supports both static and reactive target values.
227
+ * Automatically cleans up when the enclosing effect is disposed.
228
+ *
229
+ * @param target Target value (number) or reactive function returning a number
230
+ * @param options Spring physics configuration
231
+ * @returns Spring result with reactive value and controls
232
+ *
233
+ * @example
234
+ * // Static target
235
+ * const spring = useSpring(100, { stiffness: 170, damping: 26 });
236
+ * spring.set(200); // Animate to 200
237
+ *
238
+ * // Reactive target
239
+ * const x = pulse(0);
240
+ * const spring = useSpring(() => x.get(), { stiffness: 300 });
241
+ * x.set(100); // Spring automatically animates to 100
242
+ *
243
+ * effect(() => {
244
+ * element.style.transform = `translateX(${spring.value.get()}px)`;
245
+ * });
246
+ */
247
+ export declare function useSpring(
248
+ target: number | (() => number),
249
+ options?: SpringOptions
250
+ ): SpringResult;
251
+
252
+ // ============================================================================
253
+ // stagger()
254
+ // ============================================================================
255
+
256
+ /** Options for the stagger() function */
257
+ export interface StaggerOptions extends AnimateOptions {
258
+ /** Delay between each element's animation start in ms (default: 50) */
259
+ staggerDelay?: number;
260
+ }
261
+
262
+ /**
263
+ * Stagger an animation across multiple elements.
264
+ * Each element starts its animation after a configurable delay from the previous one.
265
+ *
266
+ * @param elements Array of elements to animate
267
+ * @param keyframes Keyframes in WAAPI format
268
+ * @param options Stagger and animation options
269
+ * @returns Array of AnimationControl objects, one per element
270
+ *
271
+ * @example
272
+ * const items = document.querySelectorAll('.list-item');
273
+ * const controls = stagger(
274
+ * Array.from(items),
275
+ * [{ opacity: 0, transform: 'translateY(20px)' }, { opacity: 1, transform: 'translateY(0)' }],
276
+ * { duration: 300, staggerDelay: 50, easing: 'ease-out' }
277
+ * );
278
+ *
279
+ * // Wait for all animations to finish
280
+ * await Promise.all(controls.map(c => c.finished));
281
+ */
282
+ export declare function stagger(
283
+ elements: HTMLElement[],
284
+ keyframes: Keyframe[] | PropertyIndexedKeyframes,
285
+ options?: StaggerOptions
286
+ ): AnimationControl[];
287
+
288
+ // ============================================================================
289
+ // Default Export
290
+ // ============================================================================
291
+
292
+ declare const _default: {
293
+ animate: typeof animate;
294
+ useTransition: typeof useTransition;
295
+ useSpring: typeof useSpring;
296
+ stagger: typeof stagger;
297
+ configureAnimations: typeof configureAnimations;
298
+ };
299
+
300
+ export default _default;