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,382 @@
1
+ /**
2
+ * Pulse Server Components (PSC) - Server Rendering Helpers
3
+ *
4
+ * Server-side helpers for rendering Server Components with async support
5
+ * and Client Component boundary detection.
6
+ *
7
+ * @module pulse-js-framework/runtime/server-components/server
8
+ */
9
+
10
+ import { markClientBoundary } from './serializer.js';
11
+ import { RuntimeError } from '../errors.js';
12
+ import { getAdapter, MockDOMAdapter, withAdapter } from '../dom-adapter.js';
13
+ import { loggers } from '../logger.js';
14
+
15
+ const log = loggers.dom;
16
+
17
+ // ============================================================================
18
+ // Server Component Rendering
19
+ // ============================================================================
20
+
21
+ /**
22
+ * Render a Server Component with support for async components.
23
+ * Executes async component functions and waits for their results.
24
+ *
25
+ * @param {Function} component - Component factory function
26
+ * @param {Object} props - Component props
27
+ * @param {Object} options - Rendering options
28
+ * @param {Set<string>} [options.clientComponents] - Set of Client Component IDs
29
+ * @param {number} [options.timeout=5000] - Timeout for async components (ms)
30
+ * @returns {Promise<Node>} Rendered DOM node
31
+ *
32
+ * @example
33
+ * async function ServerProductPage({ productId }) {
34
+ * const product = await db.products.findById(productId);
35
+ * return el('.product', product.name);
36
+ * }
37
+ *
38
+ * const node = await renderServerComponent(ServerProductPage, { productId: 123 });
39
+ */
40
+ export async function renderServerComponent(component, props = {}, options = {}) {
41
+ const timeout = options.timeout || 5000;
42
+ const clientComponents = options.clientComponents || new Set();
43
+
44
+ try {
45
+ // Execute component (may be async)
46
+ const result = await executeAsyncComponent(component, props, timeout);
47
+
48
+ // Mark Client Component boundaries in the tree
49
+ if (result && clientComponents.size > 0) {
50
+ markClientBoundaries(result, clientComponents);
51
+ }
52
+
53
+ return result;
54
+
55
+ } catch (error) {
56
+ // Check if it's a timeout error
57
+ const isTimeout = error.code === 'PSC_ASYNC_COMPONENT_TIMEOUT';
58
+
59
+ throw new RuntimeError(
60
+ isTimeout
61
+ ? `Server Component rendering timed out after ${timeout}ms`
62
+ : 'Server Component rendering failed',
63
+ {
64
+ code: isTimeout ? 'PSC_SERVER_RENDER_TIMEOUT' : 'PSC_SERVER_RENDER_FAILED',
65
+ context: `Component: ${component.name || 'anonymous'}`,
66
+ suggestion: isTimeout
67
+ ? `Increase timeout or optimize component: ${component.name || 'anonymous'}`
68
+ : 'Check component for errors or timeout issues'
69
+ }
70
+ );
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Execute an async Server Component with timeout.
76
+ * Handles both sync and async component functions.
77
+ *
78
+ * @param {Function} component - Component factory
79
+ * @param {Object} props - Component props
80
+ * @param {number} timeout - Timeout in milliseconds
81
+ * @returns {Promise<any>} Component result
82
+ *
83
+ * @example
84
+ * async function MyComponent(props) {
85
+ * const data = await fetchData(props.id);
86
+ * return el('div', data.name);
87
+ * }
88
+ *
89
+ * const result = await executeAsyncComponent(MyComponent, { id: 1 }, 5000);
90
+ */
91
+ export async function executeAsyncComponent(component, props, timeout = 5000) {
92
+ if (typeof component !== 'function') {
93
+ throw new RuntimeError('Component must be a function', {
94
+ code: 'PSC_INVALID_COMPONENT',
95
+ context: `Received: ${typeof component}`
96
+ });
97
+ }
98
+
99
+ // Execute component
100
+ const resultPromise = Promise.resolve(component(props));
101
+
102
+ // Race with timeout
103
+ const timeoutPromise = new Promise((_, reject) => {
104
+ setTimeout(() => {
105
+ reject(new Error(`Component execution timed out after ${timeout}ms`));
106
+ }, timeout);
107
+ });
108
+
109
+ try {
110
+ const result = await Promise.race([resultPromise, timeoutPromise]);
111
+ return result;
112
+ } catch (error) {
113
+ // Check if it's a timeout error
114
+ const isTimeout = error.message && error.message.includes('timed out');
115
+
116
+ throw new RuntimeError(
117
+ isTimeout
118
+ ? `Component execution timed out after ${timeout}ms`
119
+ : 'Async component execution failed',
120
+ {
121
+ code: isTimeout ? 'PSC_ASYNC_COMPONENT_TIMEOUT' : 'PSC_ASYNC_COMPONENT_FAILED',
122
+ context: `Component: ${component.name || 'anonymous'}`
123
+ }
124
+ );
125
+ }
126
+ }
127
+
128
+ // ============================================================================
129
+ // Client Component Boundary Detection
130
+ // ============================================================================
131
+
132
+ /**
133
+ * Detect and mark Client Component boundaries in a DOM tree.
134
+ * Walks the tree and marks elements that are Client Components.
135
+ *
136
+ * @param {Node} node - Root node to process
137
+ * @param {Set<string>} clientComponents - Set of Client Component IDs
138
+ *
139
+ * @example
140
+ * const tree = el('.app',
141
+ * ServerComponent(),
142
+ * ClientButton({ text: 'Click' }) // Will be marked as client boundary
143
+ * );
144
+ *
145
+ * markClientBoundaries(tree, new Set(['ClientButton']));
146
+ */
147
+ export function markClientBoundaries(node, clientComponents) {
148
+ if (!node || !clientComponents || clientComponents.size === 0) {
149
+ return;
150
+ }
151
+
152
+ // Check if this node is a Client Component
153
+ const componentId = detectClientComponent(node);
154
+ if (componentId && clientComponents.has(componentId)) {
155
+ // Extract props from the element
156
+ const props = extractComponentProps(node);
157
+ markClientBoundary(node, componentId, props);
158
+ return; // Don't traverse into client boundaries
159
+ }
160
+
161
+ // Recursively process children
162
+ if (node.childNodes) {
163
+ for (let i = 0; i < node.childNodes.length; i++) {
164
+ markClientBoundaries(node.childNodes[i], clientComponents);
165
+ }
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Detect if a node is a Client Component by checking for markers.
171
+ * Client Components are identified by data-component-id attribute or
172
+ * special class names.
173
+ *
174
+ * @param {Node} node - Node to check
175
+ * @returns {string|null} Component ID or null
176
+ */
177
+ function detectClientComponent(node) {
178
+ if (!node || node.nodeType !== 1) {
179
+ return null;
180
+ }
181
+
182
+ // Check for explicit component ID attribute
183
+ if (node.hasAttribute && node.hasAttribute('data-component-id')) {
184
+ return node.getAttribute('data-component-id');
185
+ }
186
+
187
+ // Check for component class marker (e.g., 'pulse-component-Button')
188
+ if (node.className && typeof node.className === 'string') {
189
+ const match = node.className.match(/pulse-component-(\w+)/);
190
+ if (match) {
191
+ return match[1];
192
+ }
193
+ }
194
+
195
+ return null;
196
+ }
197
+
198
+ /**
199
+ * Extract props from a component element.
200
+ * Reads data-prop-* attributes and constructs a props object.
201
+ *
202
+ * @param {Element} element - Component element
203
+ * @returns {Record<string, any>} Props object
204
+ */
205
+ function extractComponentProps(element) {
206
+ const props = {};
207
+
208
+ if (!element.attributes) {
209
+ return props;
210
+ }
211
+
212
+ // Extract data-prop-* attributes
213
+ for (let i = 0; i < element.attributes.length; i++) {
214
+ const attr = element.attributes[i];
215
+ const match = attr.name.match(/^data-prop-(.+)$/);
216
+ if (match) {
217
+ const propName = match[1];
218
+ try {
219
+ // Try to parse as JSON
220
+ props[propName] = JSON.parse(attr.value);
221
+ } catch {
222
+ // Fallback to string value
223
+ props[propName] = attr.value;
224
+ }
225
+ }
226
+ }
227
+
228
+ return props;
229
+ }
230
+
231
+ // ============================================================================
232
+ // Server-Side Rendering Integration
233
+ // ============================================================================
234
+
235
+ /**
236
+ * Render a Server Component tree to HTML string.
237
+ * Integrates with existing SSR infrastructure.
238
+ *
239
+ * @param {Function} component - Root component
240
+ * @param {Object} props - Component props
241
+ * @param {Object} options - Rendering options
242
+ * @returns {Promise<string>} Rendered HTML string
243
+ *
244
+ * @example
245
+ * const html = await renderServerComponentToHTML(App, { userId: 1 });
246
+ */
247
+ export async function renderServerComponentToHTML(component, props = {}, options = {}) {
248
+ const adapter = new MockDOMAdapter();
249
+
250
+ return withAdapter(adapter, async () => {
251
+ // Render the component
252
+ const node = await renderServerComponent(component, props, options);
253
+
254
+ // Append to virtual body
255
+ if (node) {
256
+ adapter.appendChild(adapter.getBody(), node);
257
+ }
258
+
259
+ // Serialize to HTML (use existing ssr-serializer)
260
+ const { serializeChildren } = await import('../ssr-serializer.js');
261
+ return serializeChildren(adapter.getBody());
262
+ });
263
+ }
264
+
265
+ // ============================================================================
266
+ // Component Registry
267
+ // ============================================================================
268
+
269
+ /**
270
+ * Component registry for server-side rendering.
271
+ * Maps component names to their factory functions.
272
+ */
273
+ class ComponentRegistry {
274
+ constructor() {
275
+ this.#components = new Map();
276
+ this.#clientComponents = new Set();
277
+ }
278
+
279
+ /** @type {Map<string, Function>} */
280
+ #components;
281
+
282
+ /** @type {Set<string>} */
283
+ #clientComponents;
284
+
285
+ /**
286
+ * Register a Server Component.
287
+ *
288
+ * @param {string} name - Component name
289
+ * @param {Function} factory - Component factory function
290
+ *
291
+ * @example
292
+ * registry.registerServer('ProductList', ProductListComponent);
293
+ */
294
+ registerServer(name, factory) {
295
+ if (this.#components.has(name)) {
296
+ log.warn(`Component '${name}' already registered, overwriting`);
297
+ }
298
+ this.#components.set(name, factory);
299
+ }
300
+
301
+ /**
302
+ * Register a Client Component.
303
+ *
304
+ * @param {string} name - Component name
305
+ * @param {Function} factory - Component factory function
306
+ *
307
+ * @example
308
+ * registry.registerClient('Button', ButtonComponent);
309
+ */
310
+ registerClient(name, factory) {
311
+ this.registerServer(name, factory); // Also register as server for SSR
312
+ this.#clientComponents.add(name);
313
+ }
314
+
315
+ /**
316
+ * Get a component by name.
317
+ *
318
+ * @param {string} name - Component name
319
+ * @returns {Function|null} Component factory or null
320
+ */
321
+ get(name) {
322
+ return this.#components.get(name) || null;
323
+ }
324
+
325
+ /**
326
+ * Check if a component is a Client Component.
327
+ *
328
+ * @param {string} name - Component name
329
+ * @returns {boolean} True if client component
330
+ */
331
+ isClientComponent(name) {
332
+ return this.#clientComponents.has(name);
333
+ }
334
+
335
+ /**
336
+ * Get all Client Component IDs.
337
+ *
338
+ * @returns {Set<string>} Set of client component names
339
+ */
340
+ getClientComponents() {
341
+ return new Set(this.#clientComponents);
342
+ }
343
+
344
+ /**
345
+ * Clear the registry.
346
+ */
347
+ clear() {
348
+ this.#components.clear();
349
+ this.#clientComponents.clear();
350
+ }
351
+ }
352
+
353
+ /**
354
+ * Global component registry instance.
355
+ */
356
+ export const componentRegistry = new ComponentRegistry();
357
+
358
+ /**
359
+ * Create a new isolated component registry.
360
+ *
361
+ * @returns {ComponentRegistry} New registry instance
362
+ *
363
+ * @example
364
+ * const registry = createComponentRegistry();
365
+ * registry.registerServer('MyComponent', MyComponentFactory);
366
+ */
367
+ export function createComponentRegistry() {
368
+ return new ComponentRegistry();
369
+ }
370
+
371
+ // ============================================================================
372
+ // Exports
373
+ // ============================================================================
374
+
375
+ export default {
376
+ renderServerComponent,
377
+ executeAsyncComponent,
378
+ markClientBoundaries,
379
+ renderServerComponentToHTML,
380
+ componentRegistry,
381
+ createComponentRegistry
382
+ };