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.
- package/README.md +11 -0
- package/cli/build.js +13 -3
- package/compiler/directives.js +356 -0
- package/compiler/lexer.js +18 -3
- package/compiler/parser/core.js +6 -0
- package/compiler/parser/view.js +2 -6
- package/compiler/preprocessor.js +43 -23
- package/compiler/sourcemap.js +3 -1
- package/compiler/transformer/actions.js +329 -0
- package/compiler/transformer/export.js +7 -0
- package/compiler/transformer/expressions.js +85 -33
- package/compiler/transformer/imports.js +3 -0
- package/compiler/transformer/index.js +2 -0
- package/compiler/transformer/store.js +1 -1
- package/compiler/transformer/style.js +45 -16
- package/compiler/transformer/view.js +23 -2
- package/loader/rollup-plugin-server-components.js +391 -0
- package/loader/vite-plugin-server-components.js +420 -0
- package/loader/webpack-loader-server-components.js +356 -0
- package/package.json +124 -82
- package/runtime/async.js +4 -0
- package/runtime/context.js +16 -3
- package/runtime/dom-adapter.js +5 -3
- package/runtime/dom-virtual-list.js +2 -1
- package/runtime/form.js +8 -3
- package/runtime/graphql/cache.js +1 -1
- package/runtime/graphql/client.js +22 -0
- package/runtime/graphql/hooks.js +12 -6
- package/runtime/graphql/subscriptions.js +2 -0
- package/runtime/hmr.js +6 -3
- package/runtime/http.js +1 -0
- package/runtime/i18n.js +2 -0
- package/runtime/lru-cache.js +3 -1
- package/runtime/native.js +46 -20
- package/runtime/pulse.js +3 -0
- package/runtime/router/core.js +5 -1
- package/runtime/router/index.js +17 -1
- package/runtime/router/psc-integration.js +301 -0
- package/runtime/security.js +58 -29
- package/runtime/server-components/actions-server.js +798 -0
- package/runtime/server-components/actions.js +389 -0
- package/runtime/server-components/client.js +447 -0
- package/runtime/server-components/error-sanitizer.js +438 -0
- package/runtime/server-components/index.js +275 -0
- package/runtime/server-components/security-csrf.js +593 -0
- package/runtime/server-components/security-errors.js +227 -0
- package/runtime/server-components/security-ratelimit.js +733 -0
- package/runtime/server-components/security-validation.js +467 -0
- package/runtime/server-components/security.js +598 -0
- package/runtime/server-components/serializer.js +617 -0
- package/runtime/server-components/server.js +382 -0
- package/runtime/server-components/types.js +383 -0
- package/runtime/server-components/utils/mutex.js +60 -0
- package/runtime/server-components/utils/path-sanitizer.js +109 -0
- package/runtime/ssr.js +2 -1
- package/runtime/store.js +19 -10
- package/runtime/utils.js +12 -128
- package/types/animation.d.ts +300 -0
- package/types/i18n.d.ts +283 -0
- package/types/persistence.d.ts +267 -0
- package/types/sse.d.ts +248 -0
- package/types/sw.d.ts +150 -0
- package/runtime/a11y.js.original +0 -1844
- package/runtime/graphql.js.original +0 -1326
- 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
|
+
};
|