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,391 @@
1
+ /**
2
+ * Pulse Server Components - Rollup Plugin
3
+ *
4
+ * Enables automatic code splitting for Client Components marked with 'use client'
5
+ * and generates a client manifest for Server Components architecture.
6
+ *
7
+ * Features:
8
+ * - Detects Client Components via __directive metadata
9
+ * - Creates separate chunks for Client Components
10
+ * - Generates client manifest (component ID → chunk URL mapping)
11
+ *
12
+ * Usage:
13
+ * ```javascript
14
+ * import pulsePlugin from 'pulse-js-framework/rollup';
15
+ * import pulseServerComponents from 'pulse-js-framework/rollup/server-components';
16
+ *
17
+ * export default {
18
+ * input: 'src/main.js',
19
+ * output: { dir: 'dist', format: 'es' },
20
+ * plugins: [
21
+ * pulsePlugin(),
22
+ * pulseServerComponents()
23
+ * ]
24
+ * };
25
+ * ```
26
+ *
27
+ * @module pulse-js-framework/loader/rollup-plugin-server-components
28
+ */
29
+
30
+ import { writeFileSync, mkdirSync } from 'fs';
31
+ import { dirname, relative, posix } from 'path';
32
+ import {
33
+ getComponentTypeFromSource
34
+ } from '../compiler/directives.js';
35
+
36
+ /**
37
+ * Default options for Server Components plugin
38
+ */
39
+ const DEFAULT_OPTIONS = {
40
+ // Output directory for client manifest
41
+ manifestPath: 'dist/.pulse-manifest.json',
42
+
43
+ // Public base path for chunk URLs (empty for relative paths)
44
+ base: '',
45
+
46
+ // Custom manifest filename
47
+ manifestFilename: '.pulse-manifest.json'
48
+ };
49
+
50
+ /**
51
+ * Create Pulse Server Components Rollup plugin
52
+ *
53
+ * @param {Object} options - Plugin options
54
+ * @param {string} [options.manifestPath] - Path to output client manifest
55
+ * @param {string} [options.base] - Base URL for chunk paths
56
+ * @param {string} [options.manifestFilename] - Manifest filename
57
+ * @returns {Object} Rollup plugin
58
+ */
59
+ export default function pulseServerComponentsPlugin(options = {}) {
60
+ const config = { ...DEFAULT_OPTIONS, ...options };
61
+
62
+ // Store detected Client Components during build
63
+ const clientComponents = new Map(); // componentId → { file, chunk }
64
+
65
+ // Track component types: filePath → 'client' | 'server' | 'shared'
66
+ const componentTypes = new Map();
67
+
68
+ // Track imports: filePath → Set<importPath>
69
+ const importGraph = new Map();
70
+
71
+ return {
72
+ name: 'rollup-plugin-pulse-server-components',
73
+
74
+ /**
75
+ * Analyze transformed modules to detect Client Components and build import graph
76
+ */
77
+ transform(code, id) {
78
+ // Determine component type for all JS/TS files
79
+ if (/\.(js|ts|jsx|tsx|pulse)$/.test(id)) {
80
+ const componentType = getComponentTypeFromSource(code, id);
81
+ componentTypes.set(id, componentType);
82
+
83
+ // Extract imports for validation
84
+ const imports = extractImports(code);
85
+ if (!importGraph.has(id)) {
86
+ importGraph.set(id, new Set());
87
+ }
88
+ for (const imp of imports) {
89
+ importGraph.get(id).add(imp);
90
+ }
91
+ }
92
+
93
+ // Only process .pulse files (after they've been transformed by pulse plugin)
94
+ if (!id.endsWith('.pulse')) {
95
+ return null;
96
+ }
97
+
98
+ // Check if this module exports a Client Component
99
+ // Look for: __directive: "use client"
100
+ const directiveMatch = code.match(/__directive:\s*["']use client["']/);
101
+
102
+ if (directiveMatch) {
103
+ // Extract component ID from export
104
+ const exportMatch = code.match(/export const (\w+) = \{/);
105
+ const componentIdMatch = code.match(/__componentId:\s*["'](\w+)["']/);
106
+
107
+ const componentId = componentIdMatch ? componentIdMatch[1] : (exportMatch ? exportMatch[1] : null);
108
+
109
+ if (componentId) {
110
+ // Store this as a Client Component
111
+ clientComponents.set(componentId, {
112
+ file: id,
113
+ chunk: null // Will be filled in during generateBundle
114
+ });
115
+
116
+ console.log(`[Pulse Server Components] Detected Client Component: ${componentId} (${relative(process.cwd(), id)})`);
117
+ }
118
+ }
119
+
120
+ return null; // Don't modify the code
121
+ },
122
+
123
+ /**
124
+ * Validate imports after all modules are transformed
125
+ */
126
+ async buildEnd() {
127
+ // Validate Client → Server import violations
128
+ for (const [filePath, componentType] of componentTypes.entries()) {
129
+ if (componentType === 'client') {
130
+ const imports = importGraph.get(filePath) || new Set();
131
+
132
+ for (const importPath of imports) {
133
+ // Resolve import to absolute path
134
+ try {
135
+ const resolved = await this.resolve(importPath, filePath, { skipSelf: true });
136
+ if (!resolved || resolved.external) continue;
137
+
138
+ const resolvedId = resolved.id;
139
+ const importedType = componentTypes.get(resolvedId);
140
+
141
+ // Check for Client → Server violation
142
+ if (importedType === 'server') {
143
+ this.error(createImportViolationError(filePath, resolvedId, importPath));
144
+ }
145
+ } catch (err) {
146
+ // Ignore resolution errors (might be external packages)
147
+ continue;
148
+ }
149
+ }
150
+ }
151
+ }
152
+ },
153
+
154
+ /**
155
+ * Configure output to create separate chunks for Client Components
156
+ */
157
+ outputOptions(outputOptions) {
158
+ // Store original manualChunks function (if any)
159
+ const originalManualChunks = outputOptions.manualChunks;
160
+
161
+ // Override manualChunks to add Client Component splitting
162
+ outputOptions.manualChunks = (id, api) => {
163
+ // Check if this file is a Client Component
164
+ for (const [componentId, info] of clientComponents.entries()) {
165
+ if (id === info.file) {
166
+ // Create a separate chunk for this Client Component
167
+ return `client-${componentId}`;
168
+ }
169
+ }
170
+
171
+ // Fall back to original manualChunks if provided
172
+ if (typeof originalManualChunks === 'function') {
173
+ return originalManualChunks(id, api);
174
+ } else if (originalManualChunks && typeof originalManualChunks === 'object') {
175
+ // Handle manualChunks as object
176
+ for (const [chunkName, moduleIds] of Object.entries(originalManualChunks)) {
177
+ if (Array.isArray(moduleIds) && moduleIds.includes(id)) {
178
+ return chunkName;
179
+ }
180
+ }
181
+ }
182
+
183
+ return undefined;
184
+ };
185
+
186
+ return outputOptions;
187
+ },
188
+
189
+ /**
190
+ * Generate client manifest after bundle is created
191
+ */
192
+ generateBundle(outputOptions, bundle) {
193
+ // Skip if no Client Components detected
194
+ if (clientComponents.size === 0) {
195
+ return;
196
+ }
197
+
198
+ // Map chunk names to their final filenames
199
+ for (const [fileName, chunk] of Object.entries(bundle)) {
200
+ if (chunk.type === 'chunk') {
201
+ // Check if this chunk corresponds to a Client Component
202
+ for (const [componentId, info] of clientComponents.entries()) {
203
+ // Match by chunk name or by checking if the component file is in the chunk
204
+ if (chunk.name === `client-${componentId}` ||
205
+ (chunk.facadeModuleId && chunk.facadeModuleId === info.file)) {
206
+
207
+ // Store the chunk filename
208
+ info.chunk = fileName;
209
+
210
+ console.log(`[Pulse Server Components] Mapped ${componentId} → ${fileName}`);
211
+ }
212
+ }
213
+ }
214
+ }
215
+
216
+ // Build client manifest
217
+ const manifest = {
218
+ version: '1.0',
219
+ components: {}
220
+ };
221
+
222
+ for (const [componentId, info] of clientComponents.entries()) {
223
+ if (info.chunk) {
224
+ const base = config.base || '';
225
+ const chunkUrl = posix.join(base, info.chunk);
226
+
227
+ manifest.components[componentId] = {
228
+ id: componentId,
229
+ chunk: chunkUrl,
230
+ exports: ['default', componentId]
231
+ };
232
+ }
233
+ }
234
+
235
+ // Emit manifest as JSON asset
236
+ const manifestJson = JSON.stringify(manifest, null, 2);
237
+
238
+ this.emitFile({
239
+ type: 'asset',
240
+ fileName: config.manifestFilename,
241
+ source: manifestJson
242
+ });
243
+
244
+ console.log(`[Pulse Server Components] Generated client manifest with ${clientComponents.size} components`);
245
+ },
246
+
247
+ /**
248
+ * Write manifest to file system after build completes
249
+ */
250
+ closeBundle() {
251
+ // Skip if no Client Components detected
252
+ if (clientComponents.size === 0) {
253
+ return;
254
+ }
255
+
256
+ // Build manifest object
257
+ const manifest = {
258
+ version: '1.0',
259
+ components: {}
260
+ };
261
+
262
+ for (const [componentId, info] of clientComponents.entries()) {
263
+ if (info.chunk) {
264
+ const base = config.base || '';
265
+ const chunkUrl = posix.join(base, info.chunk);
266
+
267
+ manifest.components[componentId] = {
268
+ id: componentId,
269
+ chunk: chunkUrl,
270
+ exports: ['default', componentId]
271
+ };
272
+ }
273
+ }
274
+
275
+ // Write to file system (in addition to emitted asset)
276
+ if (config.manifestPath) {
277
+ try {
278
+ const manifestDir = dirname(config.manifestPath);
279
+ mkdirSync(manifestDir, { recursive: true });
280
+ writeFileSync(config.manifestPath, JSON.stringify(manifest, null, 2), 'utf-8');
281
+ console.log(`[Pulse Server Components] Manifest written to ${config.manifestPath}`);
282
+ } catch (error) {
283
+ console.warn(`[Pulse Server Components] Failed to write manifest: ${error.message}`);
284
+ }
285
+ }
286
+ }
287
+ };
288
+ }
289
+
290
+ // ============================================================================
291
+ // Helper Functions
292
+ // ============================================================================
293
+
294
+ /**
295
+ * Extract import statements from source code
296
+ * @param {string} code - Source code
297
+ * @returns {Array<string>} Import sources
298
+ */
299
+ function extractImports(code) {
300
+ const imports = [];
301
+
302
+ // Match ES6 import statements
303
+ const importRegex = /import\s+(?:{[^}]*}|[\w$]+|\*\s+as\s+[\w$]+)\s+from\s+['"]([^'"]+)['"]/g;
304
+ let match;
305
+
306
+ while ((match = importRegex.exec(code)) !== null) {
307
+ imports.push(match[1]);
308
+ }
309
+
310
+ // Match dynamic imports
311
+ const dynamicImportRegex = /import\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
312
+ while ((match = dynamicImportRegex.exec(code)) !== null) {
313
+ imports.push(match[1]);
314
+ }
315
+
316
+ return imports;
317
+ }
318
+
319
+ /**
320
+ * Create import violation error message
321
+ * @param {string} clientPath - Client Component path
322
+ * @param {string} serverPath - Server Component path
323
+ * @param {string} importSource - Import statement source
324
+ * @returns {string} Error message
325
+ */
326
+ function createImportViolationError(clientPath, serverPath, importSource) {
327
+ const clientRelative = relative(process.cwd(), clientPath);
328
+ const serverRelative = relative(process.cwd(), serverPath);
329
+
330
+ return `
331
+ [Pulse] Import Violation: Client Component cannot import Server Component
332
+ at ${clientRelative}
333
+ importing ${importSource}
334
+ resolved to ${serverRelative}
335
+
336
+ Client Components can only import:
337
+ • Other Client Components ('use client')
338
+ • Shared utilities (no directive)
339
+ • Third-party packages
340
+
341
+ → Move shared logic to a Client Component
342
+ → Use Server Actions for server-side operations
343
+ → Create a wrapper Client Component that calls Server Actions
344
+
345
+ See: https://pulse-js.fr/server-components#import-rules
346
+ `.trim();
347
+ }
348
+
349
+ /**
350
+ * Helper function to load client manifest (for SSR)
351
+ *
352
+ * @param {string} manifestPath - Path to manifest file
353
+ * @returns {Object} Client manifest
354
+ *
355
+ * @example
356
+ * import { loadClientManifest } from 'pulse-js-framework/rollup/server-components';
357
+ *
358
+ * const manifest = loadClientManifest('./dist/.pulse-manifest.json');
359
+ * // Use manifest for SSR: renderServerComponent(Component, props, { clientManifest: manifest.components })
360
+ */
361
+ export function loadClientManifest(manifestPath) {
362
+ try {
363
+ const { readFileSync } = require('fs');
364
+ const content = readFileSync(manifestPath, 'utf-8');
365
+ return JSON.parse(content);
366
+ } catch (error) {
367
+ console.warn(`Failed to load client manifest from ${manifestPath}:`, error.message);
368
+ return { version: '1.0', components: {} };
369
+ }
370
+ }
371
+
372
+ /**
373
+ * Helper function to get client component chunk URL
374
+ *
375
+ * @param {Object} manifest - Client manifest
376
+ * @param {string} componentId - Component ID
377
+ * @returns {string|null} Chunk URL or null if not found
378
+ */
379
+ export function getComponentChunk(manifest, componentId) {
380
+ return manifest.components[componentId]?.chunk || null;
381
+ }
382
+
383
+ /**
384
+ * Helper function to get all client component IDs
385
+ *
386
+ * @param {Object} manifest - Client manifest
387
+ * @returns {Set<string>} Set of component IDs
388
+ */
389
+ export function getClientComponentIds(manifest) {
390
+ return new Set(Object.keys(manifest.components));
391
+ }