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,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
|
+
};
|