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,447 @@
1
+ /**
2
+ * Pulse Server Components (PSC) - Client Reconstructor
3
+ *
4
+ * Reconstructs DOM trees from PSC Wire Format on the client side.
5
+ * Handles lazy-loading of Client Component chunks and hydration.
6
+ *
7
+ * @module pulse-js-framework/runtime/server-components/client
8
+ */
9
+
10
+ import { PSCNodeType, isPSCElement, isPSCText, isPSCClientBoundary, isPSCFragment, isPSCComment } from './types.js';
11
+ import { RuntimeError } from '../errors.js';
12
+ import { getAdapter } from '../dom-adapter.js';
13
+ import { loggers } from '../logger.js';
14
+
15
+ const log = loggers.dom;
16
+
17
+ // ============================================================================
18
+ // Client Component Cache
19
+ // ============================================================================
20
+
21
+ /** Cache of loaded Client Component modules */
22
+ const componentCache = new Map();
23
+
24
+ /** In-flight component loads (Promise cache to prevent duplicate requests) */
25
+ const loadingComponents = new Map();
26
+
27
+ // ============================================================================
28
+ // PSC Reconstruction
29
+ // ============================================================================
30
+
31
+ /**
32
+ * Reconstruct a PSC node to DOM node(s).
33
+ *
34
+ * @param {PSCNode} pscNode - PSC node to reconstruct
35
+ * @param {Object} options - Reconstruction options
36
+ * @param {Record<string, Function>} [options.clientComponents] - Pre-loaded Client Components
37
+ * @param {Record<string, ClientManifestEntry>} [options.clientManifest] - Client manifest
38
+ * @returns {Promise<Node|Node[]>} Reconstructed DOM node(s)
39
+ *
40
+ * @example
41
+ * const domNode = await reconstructNode(pscNode, {
42
+ * clientComponents: { Button: ButtonComponent },
43
+ * clientManifest: { Button: { chunk: '/Button.js', exports: ['default'] } }
44
+ * });
45
+ */
46
+ export async function reconstructNode(pscNode, options = {}) {
47
+ if (!pscNode) {
48
+ return null;
49
+ }
50
+
51
+ const adapter = getAdapter();
52
+
53
+ // Element node
54
+ if (isPSCElement(pscNode)) {
55
+ return reconstructElement(pscNode, options, adapter);
56
+ }
57
+
58
+ // Text node
59
+ if (isPSCText(pscNode)) {
60
+ return reconstructText(pscNode, adapter);
61
+ }
62
+
63
+ // Client Component boundary
64
+ if (isPSCClientBoundary(pscNode)) {
65
+ return reconstructClientBoundary(pscNode, options, adapter);
66
+ }
67
+
68
+ // Fragment
69
+ if (isPSCFragment(pscNode)) {
70
+ return reconstructFragment(pscNode, options, adapter);
71
+ }
72
+
73
+ // Comment
74
+ if (isPSCComment(pscNode)) {
75
+ return reconstructComment(pscNode, adapter);
76
+ }
77
+
78
+ log.warn('Unknown PSC node type:', pscNode.type);
79
+ return null;
80
+ }
81
+
82
+ /**
83
+ * Reconstruct a PSC element to DOM element.
84
+ *
85
+ * @param {PSCElement} pscElement - PSC element
86
+ * @param {Object} options - Options
87
+ * @param {any} adapter - DOM adapter
88
+ * @returns {Promise<Element>} DOM element
89
+ */
90
+ async function reconstructElement(pscElement, options, adapter) {
91
+ const element = adapter.createElement(pscElement.tag);
92
+
93
+ // Set attributes/properties
94
+ if (pscElement.props) {
95
+ for (const [key, value] of Object.entries(pscElement.props)) {
96
+ adapter.setAttribute(element, key, value);
97
+ }
98
+ }
99
+
100
+ // Reconstruct children
101
+ if (pscElement.children && pscElement.children.length > 0) {
102
+ for (const childPSC of pscElement.children) {
103
+ const childNode = await reconstructNode(childPSC, options);
104
+ if (childNode) {
105
+ if (Array.isArray(childNode)) {
106
+ // Multiple nodes (from fragment)
107
+ childNode.forEach(node => adapter.appendChild(element, node));
108
+ } else {
109
+ adapter.appendChild(element, childNode);
110
+ }
111
+ }
112
+ }
113
+ }
114
+
115
+ return element;
116
+ }
117
+
118
+ /**
119
+ * Reconstruct a PSC text node to DOM text node.
120
+ *
121
+ * @param {PSCText} pscText - PSC text
122
+ * @param {any} adapter - DOM adapter
123
+ * @returns {Text} DOM text node
124
+ */
125
+ function reconstructText(pscText, adapter) {
126
+ return adapter.createTextNode(pscText.value);
127
+ }
128
+
129
+ /**
130
+ * Reconstruct a PSC comment to DOM comment.
131
+ *
132
+ * @param {PSCComment} pscComment - PSC comment
133
+ * @param {any} adapter - DOM adapter
134
+ * @returns {Comment} DOM comment node
135
+ */
136
+ function reconstructComment(pscComment, adapter) {
137
+ return adapter.createComment(pscComment.value);
138
+ }
139
+
140
+ /**
141
+ * Reconstruct a PSC fragment to array of DOM nodes.
142
+ *
143
+ * @param {PSCFragment} pscFragment - PSC fragment
144
+ * @param {Object} options - Options
145
+ * @param {any} adapter - DOM adapter
146
+ * @returns {Promise<Node[]>} Array of DOM nodes
147
+ */
148
+ async function reconstructFragment(pscFragment, options, adapter) {
149
+ const nodes = [];
150
+
151
+ if (pscFragment.children && pscFragment.children.length > 0) {
152
+ for (const childPSC of pscFragment.children) {
153
+ const childNode = await reconstructNode(childPSC, options);
154
+ if (childNode) {
155
+ if (Array.isArray(childNode)) {
156
+ nodes.push(...childNode);
157
+ } else {
158
+ nodes.push(childNode);
159
+ }
160
+ }
161
+ }
162
+ }
163
+
164
+ return nodes;
165
+ }
166
+
167
+ /**
168
+ * Reconstruct a PSC Client Component boundary.
169
+ * Lazy-loads the Client Component chunk if not already loaded.
170
+ *
171
+ * @param {PSCClientBoundary} pscBoundary - PSC client boundary
172
+ * @param {Object} options - Options
173
+ * @param {any} adapter - DOM adapter
174
+ * @returns {Promise<Node>} DOM node (component or fallback)
175
+ */
176
+ async function reconstructClientBoundary(pscBoundary, options, adapter) {
177
+ const { id, props, fallback } = pscBoundary;
178
+
179
+ try {
180
+ // Check if component is pre-loaded
181
+ let ComponentFn = options.clientComponents?.[id];
182
+
183
+ // If not pre-loaded, lazy-load from manifest
184
+ if (!ComponentFn) {
185
+ const manifest = options.clientManifest || {};
186
+ const manifestEntry = manifest[id];
187
+
188
+ if (!manifestEntry) {
189
+ throw new RuntimeError(`Client Component '${id}' not found in manifest`, {
190
+ code: 'PSC_COMPONENT_NOT_FOUND',
191
+ context: `Available components: ${Object.keys(manifest).join(', ')}`,
192
+ suggestion: 'Ensure the component is marked with "use client" directive'
193
+ });
194
+ }
195
+
196
+ ComponentFn = await loadClientComponent(id, manifestEntry.chunk);
197
+ }
198
+
199
+ // Execute Client Component with props
200
+ const componentResult = await ComponentFn(props);
201
+
202
+ // If component returns a DOM node, use it directly
203
+ if (componentResult && typeof componentResult === 'object') {
204
+ return componentResult;
205
+ }
206
+
207
+ throw new RuntimeError(`Client Component '${id}' did not return a DOM node`, {
208
+ code: 'PSC_INVALID_COMPONENT_RETURN',
209
+ context: `Returned: ${typeof componentResult}`
210
+ });
211
+
212
+ } catch (error) {
213
+ log.error(`Failed to reconstruct Client Component '${id}':`, error.message);
214
+
215
+ // Render fallback if provided
216
+ if (fallback) {
217
+ return reconstructNode(fallback, options);
218
+ }
219
+
220
+ // Return error placeholder
221
+ const errorEl = adapter.createElement('div');
222
+ adapter.setAttribute(errorEl, 'class', 'pulse-client-error');
223
+ adapter.setAttribute(errorEl, 'data-error', error.message);
224
+ const errorText = adapter.createTextNode(`Error loading component: ${id}`);
225
+ adapter.appendChild(errorEl, errorText);
226
+ return errorEl;
227
+ }
228
+ }
229
+
230
+ // ============================================================================
231
+ // Client Component Loading
232
+ // ============================================================================
233
+
234
+ /**
235
+ * Lazy-load a Client Component chunk from the given URL.
236
+ * Uses dynamic import() and caches the result.
237
+ *
238
+ * @param {string} componentId - Component ID
239
+ * @param {string} chunkUrl - URL to the component JS chunk
240
+ * @returns {Promise<Function>} Component function
241
+ *
242
+ * @example
243
+ * const Button = await loadClientComponent('Button', '/assets/Button.js');
244
+ * const buttonEl = Button({ text: 'Click me' });
245
+ */
246
+ export async function loadClientComponent(componentId, chunkUrl) {
247
+ // Check cache
248
+ if (componentCache.has(componentId)) {
249
+ return componentCache.get(componentId);
250
+ }
251
+
252
+ // Check if already loading
253
+ if (loadingComponents.has(componentId)) {
254
+ return loadingComponents.get(componentId);
255
+ }
256
+
257
+ // Start loading
258
+ const loadPromise = (async () => {
259
+ try {
260
+ log.debug(`Loading Client Component '${componentId}' from ${chunkUrl}`);
261
+
262
+ // Dynamic import
263
+ const module = await import(/* @vite-ignore */ chunkUrl);
264
+
265
+ // Get default export or named export matching component ID
266
+ let ComponentFn = module.default || module[componentId];
267
+
268
+ if (!ComponentFn) {
269
+ throw new RuntimeError(
270
+ `Client Component '${componentId}' not found in module`,
271
+ {
272
+ code: 'PSC_COMPONENT_EXPORT_NOT_FOUND',
273
+ context: `Available exports: ${Object.keys(module).join(', ')}`,
274
+ suggestion: 'Ensure the component is exported as default or with matching name'
275
+ }
276
+ );
277
+ }
278
+
279
+ // Cache the loaded component
280
+ componentCache.set(componentId, ComponentFn);
281
+ loadingComponents.delete(componentId);
282
+
283
+ return ComponentFn;
284
+
285
+ } catch (error) {
286
+ loadingComponents.delete(componentId);
287
+
288
+ throw new RuntimeError(
289
+ `Failed to load Client Component '${componentId}'`,
290
+ {
291
+ code: 'PSC_COMPONENT_LOAD_FAILED',
292
+ context: `URL: ${chunkUrl}`,
293
+ suggestion: 'Check that the chunk URL is correct and the file exists',
294
+ cause: error
295
+ }
296
+ );
297
+ }
298
+ })();
299
+
300
+ loadingComponents.set(componentId, loadPromise);
301
+ return loadPromise;
302
+ }
303
+
304
+ /**
305
+ * Preload a Client Component chunk (without executing it).
306
+ * Useful for prefetching components on hover or intersection.
307
+ *
308
+ * @param {string} componentId - Component ID
309
+ * @param {string} chunkUrl - URL to the component JS chunk
310
+ * @returns {Promise<void>}
311
+ *
312
+ * @example
313
+ * // Prefetch on hover
314
+ * element.addEventListener('mouseenter', () => {
315
+ * preloadClientComponent('Dashboard', '/assets/Dashboard.js');
316
+ * });
317
+ */
318
+ export async function preloadClientComponent(componentId, chunkUrl) {
319
+ if (componentCache.has(componentId)) {
320
+ return; // Already loaded
321
+ }
322
+
323
+ // Use link rel="modulepreload" for faster loading
324
+ if (typeof document !== 'undefined' && document.createElement) {
325
+ const link = document.createElement('link');
326
+ link.rel = 'modulepreload';
327
+ link.href = chunkUrl;
328
+ document.head.appendChild(link);
329
+ }
330
+
331
+ // Also start the actual load
332
+ await loadClientComponent(componentId, chunkUrl);
333
+ }
334
+
335
+ // ============================================================================
336
+ // Full PSC Tree Reconstruction
337
+ // ============================================================================
338
+
339
+ /**
340
+ * Reconstruct a complete PSC payload to DOM tree.
341
+ * This is the main entry point for PSC reconstruction on the client.
342
+ *
343
+ * @param {PSCPayload} payload - Complete PSC payload from server
344
+ * @returns {Promise<Node>} Reconstructed DOM tree
345
+ *
346
+ * @example
347
+ * const response = await fetch('/products/123?_psc=1');
348
+ * const payload = await response.json();
349
+ * const domTree = await reconstructPSCTree(payload);
350
+ * document.getElementById('app').replaceChildren(domTree);
351
+ */
352
+ export async function reconstructPSCTree(payload) {
353
+ if (!payload || !payload.root) {
354
+ throw new RuntimeError('Invalid PSC payload: missing root', {
355
+ code: 'PSC_INVALID_PAYLOAD'
356
+ });
357
+ }
358
+
359
+ // Reconstruct the root node
360
+ const root = await reconstructNode(payload.root, {
361
+ clientManifest: payload.clientManifest || {},
362
+ clientComponents: {}
363
+ });
364
+
365
+ return root;
366
+ }
367
+
368
+ // ============================================================================
369
+ // Client Component Hydration
370
+ // ============================================================================
371
+
372
+ /**
373
+ * Hydrate Client Components in a DOM tree.
374
+ * Attaches event listeners and connects to reactive state.
375
+ *
376
+ * This is called after PSC reconstruction to make Client Components interactive.
377
+ *
378
+ * @param {Node} root - Root DOM node
379
+ * @param {Record<string, Function>} clientComponents - Client Component functions
380
+ *
381
+ * @example
382
+ * const root = await reconstructPSCTree(payload);
383
+ * hydrateClientComponents(root, {
384
+ * Button: ButtonComponent,
385
+ * Chart: ChartComponent
386
+ * });
387
+ */
388
+ export function hydrateClientComponents(root, clientComponents) {
389
+ // This is a placeholder for future hydration logic
390
+ // In the current implementation, Client Components are already rendered
391
+ // during reconstruction, so no additional hydration is needed.
392
+ //
393
+ // Future enhancements:
394
+ // 1. Identify Client Component boundaries in existing SSR'd HTML
395
+ // 2. Match them with Client Component functions
396
+ // 3. Attach event listeners without re-rendering
397
+ // 4. Connect to reactive state
398
+
399
+ log.debug('Client Component hydration (placeholder)');
400
+ }
401
+
402
+ // ============================================================================
403
+ // Cache Management
404
+ // ============================================================================
405
+
406
+ /**
407
+ * Clear the Client Component cache.
408
+ * Useful for testing or forced reloading.
409
+ *
410
+ * @example
411
+ * clearComponentCache();
412
+ */
413
+ export function clearComponentCache() {
414
+ componentCache.clear();
415
+ loadingComponents.clear();
416
+ }
417
+
418
+ /**
419
+ * Get the current state of the component cache.
420
+ *
421
+ * @returns {Object} Cache statistics
422
+ *
423
+ * @example
424
+ * const stats = getComponentCacheStats();
425
+ * console.log(`Loaded: ${stats.loaded}, Loading: ${stats.loading}`);
426
+ */
427
+ export function getComponentCacheStats() {
428
+ return {
429
+ loaded: componentCache.size,
430
+ loading: loadingComponents.size,
431
+ components: Array.from(componentCache.keys())
432
+ };
433
+ }
434
+
435
+ // ============================================================================
436
+ // Exports
437
+ // ============================================================================
438
+
439
+ export default {
440
+ reconstructNode,
441
+ reconstructPSCTree,
442
+ loadClientComponent,
443
+ preloadClientComponent,
444
+ hydrateClientComponents,
445
+ clearComponentCache,
446
+ getComponentCacheStats
447
+ };