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