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