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