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,617 @@
1
+ /**
2
+ * Pulse Server Components (PSC) - Serializer
3
+ *
4
+ * Converts DOM trees (from MockDOMAdapter or BrowserDOMAdapter) to
5
+ * PSC Wire Format for transmission from server to client.
6
+ *
7
+ * @module pulse-js-framework/runtime/server-components/serializer
8
+ */
9
+
10
+ import { PSCNodeType, PSC_VERSION } from './types.js';
11
+ import { RuntimeError } from '../errors.js';
12
+ import { loggers } from '../logger.js';
13
+ import { validatePropSecurity } from './security.js';
14
+ import { validatePropSerialization } from './security-validation.js';
15
+ import { PSCSerializationError } from './security-errors.js';
16
+
17
+ const log = loggers.dom;
18
+
19
+ // ============================================================================
20
+ // Constants
21
+ // ============================================================================
22
+
23
+ /** Attribute name for marking Client Component boundaries */
24
+ const CLIENT_BOUNDARY_ATTR = 'data-pulse-client-id';
25
+
26
+ /** Attribute name for Client Component props */
27
+ const CLIENT_PROPS_ATTR = 'data-pulse-client-props';
28
+
29
+ /** Maximum serialization depth to prevent infinite recursion */
30
+ const MAX_DEPTH = 100;
31
+
32
+ // ============================================================================
33
+ // Serialization Options
34
+ // ============================================================================
35
+
36
+ /**
37
+ * @typedef {Object} SerializationOptions
38
+ * @property {Set<string>} [clientComponents] - Set of Client Component IDs
39
+ * @property {Record<string, ClientManifestEntry>} [clientManifest] - Client manifest
40
+ * @property {boolean} [includeComments=false] - Include comment nodes
41
+ * @property {number} [maxDepth=100] - Maximum tree depth
42
+ */
43
+
44
+ // ============================================================================
45
+ // Core Serialization
46
+ // ============================================================================
47
+
48
+ /**
49
+ * Serialize a DOM node to PSC Wire Format.
50
+ *
51
+ * @param {Node} node - DOM node (from adapter)
52
+ * @param {SerializationOptions} options - Serialization options
53
+ * @param {number} [depth=0] - Current recursion depth
54
+ * @returns {PSCNode|null} PSC node or null if should be skipped
55
+ *
56
+ * @example
57
+ * const pscNode = serializeNode(divElement, {
58
+ * clientComponents: new Set(['Button']),
59
+ * clientManifest: { Button: { id: 'Button', chunk: '/Button.js', exports: ['default'] } }
60
+ * });
61
+ */
62
+ export function serializeNode(node, options = {}, depth = 0) {
63
+ if (depth > (options.maxDepth || MAX_DEPTH)) {
64
+ log.warn(`PSC serialization depth exceeded ${MAX_DEPTH}, truncating tree`);
65
+ return null;
66
+ }
67
+
68
+ if (!node) {
69
+ return null;
70
+ }
71
+
72
+ const nodeType = node.nodeType;
73
+
74
+ // Element node (nodeType = 1)
75
+ if (nodeType === 1) {
76
+ return serializeElement(node, options, depth);
77
+ }
78
+
79
+ // Text node (nodeType = 3)
80
+ if (nodeType === 3) {
81
+ return serializeText(node);
82
+ }
83
+
84
+ // Comment node (nodeType = 8)
85
+ if (nodeType === 8) {
86
+ if (options.includeComments) {
87
+ return serializeComment(node);
88
+ }
89
+ return null;
90
+ }
91
+
92
+ // Document fragment (nodeType = 11)
93
+ if (nodeType === 11) {
94
+ return serializeFragment(node, options, depth);
95
+ }
96
+
97
+ // Unknown node type
98
+ log.warn(`Unknown node type ${nodeType}, skipping`);
99
+ return null;
100
+ }
101
+
102
+ /**
103
+ * Serialize an element node to PSC format.
104
+ *
105
+ * @param {Element} element - Element node
106
+ * @param {SerializationOptions} options - Options
107
+ * @param {number} depth - Current depth
108
+ * @returns {PSCElement|PSCClientBoundary} PSC element or client boundary
109
+ */
110
+ function serializeElement(element, options, depth) {
111
+ // Check if this is a Client Component boundary
112
+ if (isClientBoundary(element)) {
113
+ return serializeClientBoundary(element, options, depth);
114
+ }
115
+
116
+ // Regular server element
117
+ const tag = element.tagName ? element.tagName.toLowerCase() : 'div';
118
+ const props = serializeProps(element);
119
+ const children = serializeChildren(element, options, depth);
120
+
121
+ return {
122
+ type: PSCNodeType.ELEMENT,
123
+ tag,
124
+ props,
125
+ children
126
+ };
127
+ }
128
+
129
+ /**
130
+ * Serialize a text node to PSC format.
131
+ *
132
+ * @param {Text} textNode - Text node
133
+ * @returns {PSCText|null} PSC text or null if empty
134
+ */
135
+ function serializeText(textNode) {
136
+ const value = textNode.nodeValue || textNode.textContent || textNode.data || '';
137
+ // Skip empty text nodes
138
+ if (!value.trim()) {
139
+ return null;
140
+ }
141
+ return {
142
+ type: PSCNodeType.TEXT,
143
+ value
144
+ };
145
+ }
146
+
147
+ /**
148
+ * Serialize a comment node to PSC format.
149
+ *
150
+ * @param {Comment} commentNode - Comment node
151
+ * @returns {PSCComment} PSC comment
152
+ */
153
+ function serializeComment(commentNode) {
154
+ const value = commentNode.nodeValue || commentNode.data || '';
155
+ return {
156
+ type: PSCNodeType.COMMENT,
157
+ value
158
+ };
159
+ }
160
+
161
+ /**
162
+ * Serialize a document fragment to PSC format.
163
+ *
164
+ * @param {DocumentFragment} fragment - Fragment node
165
+ * @param {SerializationOptions} options - Options
166
+ * @param {number} depth - Current depth
167
+ * @returns {PSCFragment} PSC fragment
168
+ */
169
+ function serializeFragment(fragment, options, depth) {
170
+ const children = serializeChildren(fragment, options, depth);
171
+ return {
172
+ type: PSCNodeType.FRAGMENT,
173
+ children
174
+ };
175
+ }
176
+
177
+ /**
178
+ * Serialize a Client Component boundary.
179
+ *
180
+ * @param {Element} element - Element with client boundary marker
181
+ * @param {SerializationOptions} options - Options
182
+ * @param {number} depth - Current depth
183
+ * @returns {PSCClientBoundary} PSC client boundary
184
+ */
185
+ function serializeClientBoundary(element, options, depth) {
186
+ const id = element.getAttribute(CLIENT_BOUNDARY_ATTR);
187
+ if (!id) {
188
+ throw new RuntimeError('Client boundary missing ID attribute', {
189
+ code: 'PSC_MISSING_CLIENT_ID',
190
+ context: `Element: ${element.tagName}`
191
+ });
192
+ }
193
+
194
+ // Deserialize props from data attribute
195
+ let props = {};
196
+ const propsAttr = element.getAttribute(CLIENT_PROPS_ATTR);
197
+ if (propsAttr) {
198
+ try {
199
+ props = JSON.parse(propsAttr);
200
+ } catch (err) {
201
+ throw new RuntimeError(`Failed to parse client props for '${id}'`, {
202
+ code: 'PSC_INVALID_CLIENT_PROPS',
203
+ context: `Props attribute: ${propsAttr}`,
204
+ suggestion: 'Ensure props are JSON-serializable'
205
+ });
206
+ }
207
+ }
208
+
209
+ // ========== SERIALIZATION VALIDATION (NEW) ==========
210
+ // Validate that props are JSON-serializable (no functions, symbols, etc.)
211
+ const serializationResult = validatePropSerialization(props, id, {
212
+ throwOnError: true // Throw on non-serializable types
213
+ });
214
+
215
+ // This should throw before reaching here if validation failed,
216
+ // but check anyway for safety
217
+ if (!serializationResult.valid) {
218
+ throw new PSCSerializationError(
219
+ `Cannot serialize props for Client Component '${id}'`,
220
+ {
221
+ errors: serializationResult.errors,
222
+ props
223
+ }
224
+ );
225
+ }
226
+
227
+ // Log environment variable warnings (non-blocking)
228
+ if (serializationResult.warnings.length > 0) {
229
+ log.warn(
230
+ `PSC: Environment variable(s) detected in props for Client Component '${id}':`,
231
+ serializationResult.warnings.map(w => `${w.path}: ${w.pattern} (${w.platform})`)
232
+ );
233
+ }
234
+
235
+ // ========== SECURITY VALIDATION ==========
236
+ // Comprehensive security check: secrets, XSS, size limits, env vars
237
+ const securityResult = validatePropSecurity(props, id, {
238
+ detectSecrets: true, // Warn on detected secrets
239
+ sanitizeXSS: true, // Sanitize XSS patterns
240
+ validateSizes: true, // Enforce size limits
241
+ detectEnvVars: true, // Detect environment variables (NEW)
242
+ throwOnSecrets: false // Warn only, don't block
243
+ });
244
+
245
+ // Log security warnings (secrets and env vars detected)
246
+ if (securityResult.warnings.length > 0) {
247
+ log.warn(
248
+ `PSC: Security warnings for Client Component '${id}':`,
249
+ securityResult.warnings.map(w => {
250
+ if (w.type === 'env-var') {
251
+ return `${w.path}: ${w.pattern} (${w.platform})`;
252
+ }
253
+ return `${w.path}: ${w.value} (${w.type})`;
254
+ })
255
+ );
256
+ }
257
+
258
+ // Throw on security errors (XSS, size limits)
259
+ if (!securityResult.valid) {
260
+ const firstError = securityResult.errors[0];
261
+ throw firstError; // Already a RuntimeError from security module
262
+ }
263
+
264
+ // Use sanitized props (XSS patterns removed)
265
+ props = securityResult.sanitized;
266
+
267
+ // Serialize fallback (if any children exist)
268
+ let fallback = null;
269
+ if (element.childNodes && element.childNodes.length > 0) {
270
+ const fallbackChildren = serializeChildren(element, options, depth);
271
+ if (fallbackChildren.length > 0) {
272
+ fallback = {
273
+ type: PSCNodeType.FRAGMENT,
274
+ children: fallbackChildren
275
+ };
276
+ }
277
+ }
278
+
279
+ return {
280
+ type: PSCNodeType.CLIENT,
281
+ id,
282
+ props,
283
+ fallback
284
+ };
285
+ }
286
+
287
+ // ============================================================================
288
+ // Property Serialization
289
+ // ============================================================================
290
+
291
+ /**
292
+ * Serialize element attributes/properties to props object.
293
+ *
294
+ * @param {Element} element - Element
295
+ * @returns {Record<string, any>} Props object
296
+ */
297
+ function serializeProps(element) {
298
+ const props = {};
299
+
300
+ // Handle MockElement (uses _attributes Map)
301
+ if (element._attributes && element._attributes instanceof Map) {
302
+ for (const [name, value] of element._attributes.entries()) {
303
+ // Skip internal PSC attributes
304
+ if (name === CLIENT_BOUNDARY_ATTR || name === CLIENT_PROPS_ATTR) {
305
+ continue;
306
+ }
307
+ props[name] = value;
308
+ }
309
+ }
310
+ // Handle real DOM element (uses attributes array)
311
+ else if (element.attributes) {
312
+ for (let i = 0; i < element.attributes.length; i++) {
313
+ const attr = element.attributes[i];
314
+ const name = attr.name;
315
+
316
+ // Skip internal PSC attributes
317
+ if (name === CLIENT_BOUNDARY_ATTR || name === CLIENT_PROPS_ATTR) {
318
+ continue;
319
+ }
320
+
321
+ props[name] = attr.value;
322
+ }
323
+ }
324
+
325
+ // Add className if present (MockElement uses className property)
326
+ if (element.className) {
327
+ props.class = element.className;
328
+ }
329
+
330
+ return props;
331
+ }
332
+
333
+ /**
334
+ * Validate that props are JSON-serializable (no functions, symbols, etc.).
335
+ *
336
+ * @param {Record<string, any>} props - Props to validate
337
+ * @param {string} componentId - Component ID for error messages
338
+ * @throws {RuntimeError} If props contain non-serializable values
339
+ */
340
+ function validateSerializableProps(props, componentId) {
341
+ const seen = new WeakSet();
342
+
343
+ function check(value, path) {
344
+ if (value === null || value === undefined) {
345
+ return; // Allowed
346
+ }
347
+
348
+ const type = typeof value;
349
+
350
+ // Primitives OK
351
+ if (type === 'string' || type === 'number' || type === 'boolean') {
352
+ return;
353
+ }
354
+
355
+ // Functions NOT OK
356
+ if (type === 'function') {
357
+ throw new RuntimeError(
358
+ `Client Component '${componentId}' received function prop at '${path}'`,
359
+ {
360
+ code: 'PSC_FUNCTION_PROP',
361
+ context: 'Functions cannot be serialized to client',
362
+ suggestion: 'Use Server Actions instead of inline functions'
363
+ }
364
+ );
365
+ }
366
+
367
+ // Symbols NOT OK
368
+ if (type === 'symbol') {
369
+ throw new RuntimeError(
370
+ `Client Component '${componentId}' received symbol prop at '${path}'`,
371
+ {
372
+ code: 'PSC_SYMBOL_PROP',
373
+ context: 'Symbols cannot be serialized'
374
+ }
375
+ );
376
+ }
377
+
378
+ // Objects/Arrays - check recursively
379
+ if (type === 'object') {
380
+ // Circular reference check
381
+ if (seen.has(value)) {
382
+ throw new RuntimeError(
383
+ `Client Component '${componentId}' has circular reference at '${path}'`,
384
+ {
385
+ code: 'PSC_CIRCULAR_PROP',
386
+ context: 'Props must not contain circular references'
387
+ }
388
+ );
389
+ }
390
+ seen.add(value);
391
+
392
+ // Arrays
393
+ if (Array.isArray(value)) {
394
+ value.forEach((item, i) => check(item, `${path}[${i}]`));
395
+ }
396
+ // Plain objects
397
+ else if (value.constructor === Object || value.constructor === undefined) {
398
+ for (const [key, val] of Object.entries(value)) {
399
+ check(val, `${path}.${key}`);
400
+ }
401
+ }
402
+ // Class instances with methods NOT OK
403
+ else if (typeof value.constructor === 'function' && value.constructor !== Object) {
404
+ throw new RuntimeError(
405
+ `Client Component '${componentId}' received class instance at '${path}'`,
406
+ {
407
+ code: 'PSC_CLASS_INSTANCE_PROP',
408
+ context: `Type: ${value.constructor.name}`,
409
+ suggestion: 'Pass plain objects instead of class instances'
410
+ }
411
+ );
412
+ }
413
+ }
414
+ }
415
+
416
+ check(props, 'props');
417
+ }
418
+
419
+ // ============================================================================
420
+ // Children Serialization
421
+ // ============================================================================
422
+
423
+ /**
424
+ * Serialize child nodes of an element or fragment.
425
+ *
426
+ * @param {Node} parent - Parent node
427
+ * @param {SerializationOptions} options - Options
428
+ * @param {number} depth - Current depth
429
+ * @returns {PSCNode[]} Array of serialized children
430
+ */
431
+ function serializeChildren(parent, options, depth) {
432
+ const children = [];
433
+
434
+ if (!parent.childNodes) {
435
+ return children;
436
+ }
437
+
438
+ for (let i = 0; i < parent.childNodes.length; i++) {
439
+ const child = parent.childNodes[i];
440
+ const serialized = serializeNode(child, options, depth + 1);
441
+ if (serialized) {
442
+ children.push(serialized);
443
+ }
444
+ }
445
+
446
+ return children;
447
+ }
448
+
449
+ // ============================================================================
450
+ // Full Tree Serialization
451
+ // ============================================================================
452
+
453
+ /**
454
+ * Serialize a complete DOM tree to PSC payload.
455
+ *
456
+ * @param {Node} root - Root DOM node
457
+ * @param {Object} options - Serialization options
458
+ * @param {Set<string>} [options.clientComponents] - Set of Client Component IDs
459
+ * @param {Record<string, ClientManifestEntry>} [options.clientManifest] - Client manifest
460
+ * @param {Record<string, any>} [options.state] - Optional server state
461
+ * @returns {PSCPayload} Complete PSC payload
462
+ *
463
+ * @example
464
+ * const payload = serializeToPSC(rootElement, {
465
+ * clientComponents: new Set(['Button', 'Chart']),
466
+ * clientManifest: {
467
+ * Button: { id: 'Button', chunk: '/Button.js', exports: ['default'] },
468
+ * Chart: { id: 'Chart', chunk: '/Chart.js', exports: ['default'] }
469
+ * },
470
+ * state: { user: { id: 1, name: 'Alice' } }
471
+ * });
472
+ */
473
+ export function serializeToPSC(root, options = {}) {
474
+ const clientManifest = options.clientManifest || {};
475
+ const state = options.state;
476
+
477
+ // Serialize root node
478
+ const rootNode = serializeNode(root, options);
479
+ if (!rootNode) {
480
+ throw new RuntimeError('Failed to serialize PSC root node', {
481
+ code: 'PSC_SERIALIZATION_FAILED',
482
+ context: 'Root node serialization returned null'
483
+ });
484
+ }
485
+
486
+ // Build payload
487
+ const payload = {
488
+ version: PSC_VERSION,
489
+ root: rootNode,
490
+ clientManifest
491
+ };
492
+
493
+ if (state) {
494
+ payload.state = state;
495
+ }
496
+
497
+ return payload;
498
+ }
499
+
500
+ // ============================================================================
501
+ // Client Boundary Detection
502
+ // ============================================================================
503
+
504
+ /**
505
+ * Check if a node is a Client Component boundary.
506
+ * Client boundaries are marked with data-pulse-client-id attribute.
507
+ *
508
+ * @param {Node} node - Node to check
509
+ * @returns {boolean} True if node is a Client Component boundary
510
+ *
511
+ * @example
512
+ * if (isClientBoundary(element)) {
513
+ * console.log('Found Client Component:', element.getAttribute('data-pulse-client-id'));
514
+ * }
515
+ */
516
+ export function isClientBoundary(node) {
517
+ return (
518
+ node &&
519
+ node.nodeType === 1 &&
520
+ node.hasAttribute &&
521
+ node.hasAttribute(CLIENT_BOUNDARY_ATTR)
522
+ );
523
+ }
524
+
525
+ /**
526
+ * Mark a DOM node as a Client Component boundary.
527
+ * Used during server-side rendering to mark boundaries.
528
+ *
529
+ * @param {Element} element - Element to mark
530
+ * @param {string} componentId - Client Component ID
531
+ * @param {Record<string, any>} [props] - Component props
532
+ *
533
+ * @example
534
+ * markClientBoundary(element, 'Button', { text: 'Click me' });
535
+ */
536
+ export function markClientBoundary(element, componentId, props = {}) {
537
+ if (!element || !element.setAttribute) {
538
+ throw new RuntimeError('Cannot mark client boundary on invalid element', {
539
+ code: 'PSC_INVALID_ELEMENT'
540
+ });
541
+ }
542
+
543
+ element.setAttribute(CLIENT_BOUNDARY_ATTR, componentId);
544
+
545
+ if (props && Object.keys(props).length > 0) {
546
+ // ========== SERIALIZATION VALIDATION (NEW) ==========
547
+ // Validate that props are JSON-serializable
548
+ const serializationResult = validatePropSerialization(props, componentId, {
549
+ throwOnError: true // Throw on non-serializable types
550
+ });
551
+
552
+ if (!serializationResult.valid) {
553
+ throw new PSCSerializationError(
554
+ `Cannot serialize props for Client Component '${componentId}'`,
555
+ {
556
+ errors: serializationResult.errors,
557
+ props
558
+ }
559
+ );
560
+ }
561
+
562
+ // Log environment variable warnings
563
+ if (serializationResult.warnings.length > 0) {
564
+ log.warn(
565
+ `PSC: Environment variable(s) detected when marking Client Component '${componentId}':`,
566
+ serializationResult.warnings.map(w => `${w.path}: ${w.pattern} (${w.platform})`)
567
+ );
568
+ }
569
+
570
+ // ========== SECURITY VALIDATION ==========
571
+ // Apply same security validation as during serialization
572
+ const securityResult = validatePropSecurity(props, componentId, {
573
+ detectSecrets: true,
574
+ sanitizeXSS: true,
575
+ validateSizes: true,
576
+ detectEnvVars: true, // NEW
577
+ throwOnSecrets: false
578
+ });
579
+
580
+ // Log security warnings
581
+ if (securityResult.warnings.length > 0) {
582
+ log.warn(
583
+ `PSC: Security warnings when marking Client Component '${componentId}':`,
584
+ securityResult.warnings.map(w => {
585
+ if (w.type === 'env-var') {
586
+ return `${w.path}: ${w.pattern} (${w.platform})`;
587
+ }
588
+ return `${w.path}: ${w.value} (${w.type})`;
589
+ })
590
+ );
591
+ }
592
+
593
+ // Throw on security errors
594
+ if (!securityResult.valid) {
595
+ throw securityResult.errors[0];
596
+ }
597
+
598
+ // Use sanitized props
599
+ element.setAttribute(CLIENT_PROPS_ATTR, JSON.stringify(securityResult.sanitized));
600
+ }
601
+ }
602
+
603
+ // ============================================================================
604
+ // Exports
605
+ // ============================================================================
606
+
607
+ // Export constants
608
+ export { CLIENT_BOUNDARY_ATTR, CLIENT_PROPS_ATTR };
609
+
610
+ export default {
611
+ serializeNode,
612
+ serializeToPSC,
613
+ isClientBoundary,
614
+ markClientBoundary,
615
+ CLIENT_BOUNDARY_ATTR,
616
+ CLIENT_PROPS_ATTR
617
+ };