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,356 @@
1
+ /**
2
+ * Pulse Server Components - Webpack Loader Extension
3
+ *
4
+ * Extends webpack-loader.js with Server Components support:
5
+ * - Detects Client Components via __directive metadata
6
+ * - Generates client manifest (component ID → chunk URL mapping)
7
+ * - Writes manifest to filesystem
8
+ *
9
+ * Usage in webpack.config.js:
10
+ * ```js
11
+ * const { addServerComponentsSupport } = require('pulse-js-framework/loader/webpack-loader-server-components');
12
+ *
13
+ * module.exports = {
14
+ * module: {
15
+ * rules: [
16
+ * {
17
+ * test: /\.pulse$/,
18
+ * use: [
19
+ * 'style-loader',
20
+ * 'css-loader',
21
+ * 'pulse-js-framework/loader/webpack-loader'
22
+ * ]
23
+ * }
24
+ * ]
25
+ * },
26
+ * plugins: [
27
+ * addServerComponentsSupport({
28
+ * manifestPath: 'dist/.pulse-manifest.json'
29
+ * })
30
+ * ]
31
+ * };
32
+ * ```
33
+ *
34
+ * @module pulse-js-framework/loader/webpack-loader-server-components
35
+ */
36
+
37
+ import { writeFileSync, mkdirSync } from 'fs';
38
+ import { dirname, posix, relative } from 'path';
39
+ import { getComponentTypeFromSource } from '../compiler/directives.js';
40
+
41
+ /**
42
+ * Default options for Server Components Webpack plugin
43
+ */
44
+ const DEFAULT_OPTIONS = {
45
+ // Output directory for client manifest
46
+ manifestPath: 'dist/.pulse-manifest.json',
47
+
48
+ // Public base path for chunk URLs (empty for relative paths)
49
+ base: '',
50
+
51
+ // Custom manifest filename
52
+ manifestFilename: '.pulse-manifest.json'
53
+ };
54
+
55
+ /**
56
+ * Storage for Client Components per compilation
57
+ * WeakMap<Compilation, Map<componentId, { file, chunk }>>
58
+ */
59
+ const compilationClientComponents = new WeakMap();
60
+
61
+ /**
62
+ * Get or create Client Components map for a compilation
63
+ */
64
+ function getClientComponentsMap(compilation) {
65
+ if (!compilationClientComponents.has(compilation)) {
66
+ compilationClientComponents.set(compilation, new Map());
67
+ }
68
+ return compilationClientComponents.get(compilation);
69
+ }
70
+
71
+ /**
72
+ * Webpack plugin for Pulse Server Components support
73
+ */
74
+ class PulseServerComponentsPlugin {
75
+ constructor(options = {}) {
76
+ this.options = { ...DEFAULT_OPTIONS, ...options };
77
+ }
78
+
79
+ apply(compiler) {
80
+ const pluginName = 'PulseServerComponentsPlugin';
81
+
82
+ compiler.hooks.thisCompilation.tap(pluginName, (compilation) => {
83
+ const clientComponents = getClientComponentsMap(compilation);
84
+ const componentTypes = new Map(); // module.resource → 'client' | 'server' | 'shared'
85
+
86
+ // Hook into module processing to detect Client Components and track types
87
+ compilation.hooks.succeedModule.tap(pluginName, (module) => {
88
+ if (!module.resource) return;
89
+
90
+ // Get the compiled source
91
+ const source = module._source?.source?.();
92
+ if (!source || typeof source !== 'string') {
93
+ return;
94
+ }
95
+
96
+ // Determine component type for all JS/TS files
97
+ if (/\.(js|ts|jsx|tsx|pulse)$/.test(module.resource)) {
98
+ const componentType = getComponentTypeFromSource(source, module.resource);
99
+ componentTypes.set(module.resource, componentType);
100
+ }
101
+
102
+ // Only process .pulse files for Client Component detection
103
+ if (!module.resource.endsWith('.pulse')) {
104
+ return;
105
+ }
106
+
107
+ // Check if this module exports a Client Component
108
+ // Look for: __directive: "use client"
109
+ const directiveMatch = source.match(/__directive:\s*["']use client["']/);
110
+
111
+ if (directiveMatch) {
112
+ // Extract component ID
113
+ const componentIdMatch = source.match(/__componentId:\s*["'](\w+)["']/);
114
+ const exportMatch = source.match(/export const (\w+) = \{/);
115
+
116
+ const componentId = componentIdMatch ? componentIdMatch[1] : (exportMatch ? exportMatch[1] : null);
117
+
118
+ if (componentId) {
119
+ // Store this as a Client Component
120
+ clientComponents.set(componentId, {
121
+ file: module.resource,
122
+ chunk: null, // Will be filled later
123
+ moduleId: module.id
124
+ });
125
+
126
+ const relativePath = relative(process.cwd(), module.resource);
127
+ console.log(`[Pulse Server Components] Detected Client Component: ${componentId} (${relativePath})`);
128
+ }
129
+ }
130
+ });
131
+
132
+ // Validate imports after module graph is built
133
+ compilation.hooks.finishModules.tap(pluginName, (modules) => {
134
+ for (const module of modules) {
135
+ if (!module.resource) continue;
136
+
137
+ const moduleType = componentTypes.get(module.resource);
138
+ if (moduleType !== 'client') continue;
139
+
140
+ // Check all dependencies
141
+ for (const dependency of module.dependencies || []) {
142
+ const depModule = compilation.moduleGraph?.getModule?.(dependency);
143
+ if (!depModule || !depModule.resource) continue;
144
+
145
+ const depType = componentTypes.get(depModule.resource);
146
+
147
+ // Client → Server violation
148
+ if (depType === 'server') {
149
+ const error = new Error(createImportViolationError(
150
+ module.resource,
151
+ depModule.resource,
152
+ dependency.userRequest || depModule.resource
153
+ ));
154
+ compilation.errors.push(error);
155
+ }
156
+ }
157
+ }
158
+ });
159
+
160
+ // Generate manifest after all assets are processed
161
+ compilation.hooks.processAssets.tap(
162
+ {
163
+ name: pluginName,
164
+ stage: compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_INLINE
165
+ },
166
+ (assets) => {
167
+ // Skip if no Client Components detected
168
+ if (clientComponents.size === 0) {
169
+ return;
170
+ }
171
+
172
+ // Map modules to their chunks
173
+ for (const [componentId, info] of clientComponents.entries()) {
174
+ // Find chunk(s) containing this module
175
+ for (const chunk of compilation.chunks) {
176
+ for (const module of compilation.chunkGraph.getChunkModulesIterable(chunk)) {
177
+ if (module.resource === info.file) {
178
+ // Get chunk filename
179
+ const chunkFiles = Array.from(chunk.files);
180
+ const jsFile = chunkFiles.find(f => f.endsWith('.js'));
181
+
182
+ if (jsFile) {
183
+ info.chunk = jsFile;
184
+ console.log(`[Pulse Server Components] Mapped ${componentId} → ${jsFile}`);
185
+ }
186
+ break;
187
+ }
188
+ }
189
+ }
190
+ }
191
+
192
+ // Build client manifest
193
+ const manifest = {
194
+ version: '1.0',
195
+ components: {}
196
+ };
197
+
198
+ for (const [componentId, info] of clientComponents.entries()) {
199
+ if (info.chunk) {
200
+ const base = this.options.base || '';
201
+ const chunkUrl = posix.join(base, info.chunk);
202
+
203
+ manifest.components[componentId] = {
204
+ id: componentId,
205
+ chunk: chunkUrl,
206
+ exports: ['default', componentId]
207
+ };
208
+ }
209
+ }
210
+
211
+ // Emit manifest as asset
212
+ const manifestJson = JSON.stringify(manifest, null, 2);
213
+ const { RawSource } = compilation.compiler.webpack.sources;
214
+
215
+ compilation.emitAsset(
216
+ this.options.manifestFilename,
217
+ new RawSource(manifestJson)
218
+ );
219
+
220
+ console.log(`[Pulse Server Components] Generated client manifest with ${clientComponents.size} components`);
221
+ }
222
+ );
223
+
224
+ // Write manifest to filesystem after emit
225
+ compilation.hooks.afterProcessAssets.tap(pluginName, () => {
226
+ if (clientComponents.size === 0) {
227
+ return;
228
+ }
229
+
230
+ // Build manifest object
231
+ const manifest = {
232
+ version: '1.0',
233
+ components: {}
234
+ };
235
+
236
+ for (const [componentId, info] of clientComponents.entries()) {
237
+ if (info.chunk) {
238
+ const base = this.options.base || '';
239
+ const chunkUrl = posix.join(base, info.chunk);
240
+
241
+ manifest.components[componentId] = {
242
+ id: componentId,
243
+ chunk: chunkUrl,
244
+ exports: ['default', componentId]
245
+ };
246
+ }
247
+ }
248
+
249
+ // Write to file system
250
+ if (this.options.manifestPath) {
251
+ try {
252
+ const manifestDir = dirname(this.options.manifestPath);
253
+ mkdirSync(manifestDir, { recursive: true });
254
+ writeFileSync(this.options.manifestPath, JSON.stringify(manifest, null, 2), 'utf-8');
255
+ console.log(`[Pulse Server Components] Manifest written to ${this.options.manifestPath}`);
256
+ } catch (error) {
257
+ console.warn(`[Pulse Server Components] Failed to write manifest: ${error.message}`);
258
+ }
259
+ }
260
+ });
261
+ });
262
+ }
263
+ }
264
+
265
+ // ============================================================================
266
+ // Helper Functions
267
+ // ============================================================================
268
+
269
+ /**
270
+ * Create import violation error message
271
+ * @param {string} clientPath - Client Component path
272
+ * @param {string} serverPath - Server Component path
273
+ * @param {string} importSource - Import statement source
274
+ * @returns {string} Error message
275
+ */
276
+ function createImportViolationError(clientPath, serverPath, importSource) {
277
+ const clientRelative = relative(process.cwd(), clientPath);
278
+ const serverRelative = relative(process.cwd(), serverPath);
279
+
280
+ return `
281
+ [Pulse] Import Violation: Client Component cannot import Server Component
282
+ at ${clientRelative}
283
+ importing ${importSource}
284
+ resolved to ${serverRelative}
285
+
286
+ Client Components can only import:
287
+ • Other Client Components ('use client')
288
+ • Shared utilities (no directive)
289
+ • Third-party packages
290
+
291
+ → Move shared logic to a Client Component
292
+ → Use Server Actions for server-side operations
293
+ → Create a wrapper Client Component that calls Server Actions
294
+
295
+ See: https://pulse-js.fr/server-components#import-rules
296
+ `.trim();
297
+ }
298
+
299
+ /**
300
+ * Helper function to add Server Components support to Webpack config
301
+ *
302
+ * @param {Object} options - Plugin options
303
+ * @returns {PulseServerComponentsPlugin} Webpack plugin instance
304
+ *
305
+ * @example
306
+ * const { addServerComponentsSupport } = require('pulse-js-framework/loader/webpack-loader-server-components');
307
+ *
308
+ * module.exports = {
309
+ * plugins: [
310
+ * addServerComponentsSupport({ manifestPath: 'dist/.pulse-manifest.json' })
311
+ * ]
312
+ * };
313
+ */
314
+ export function addServerComponentsSupport(options = {}) {
315
+ return new PulseServerComponentsPlugin(options);
316
+ }
317
+
318
+ /**
319
+ * Helper function to load client manifest (for SSR)
320
+ *
321
+ * @param {string} manifestPath - Path to manifest file
322
+ * @returns {Object} Client manifest
323
+ */
324
+ export function loadClientManifest(manifestPath) {
325
+ try {
326
+ const { readFileSync } = require('fs');
327
+ const content = readFileSync(manifestPath, 'utf-8');
328
+ return JSON.parse(content);
329
+ } catch (error) {
330
+ console.warn(`Failed to load client manifest from ${manifestPath}:`, error.message);
331
+ return { version: '1.0', components: {} };
332
+ }
333
+ }
334
+
335
+ /**
336
+ * Helper function to get client component chunk URL
337
+ *
338
+ * @param {Object} manifest - Client manifest
339
+ * @param {string} componentId - Component ID
340
+ * @returns {string|null} Chunk URL or null if not found
341
+ */
342
+ export function getComponentChunk(manifest, componentId) {
343
+ return manifest.components[componentId]?.chunk || null;
344
+ }
345
+
346
+ /**
347
+ * Helper function to get all client component IDs
348
+ *
349
+ * @param {Object} manifest - Client manifest
350
+ * @returns {Set<string>} Set of component IDs
351
+ */
352
+ export function getClientComponentIds(manifest) {
353
+ return new Set(Object.keys(manifest.components));
354
+ }
355
+
356
+ export default PulseServerComponentsPlugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pulse-js-framework",
3
- "version": "1.10.4",
3
+ "version": "1.11.1",
4
4
  "description": "A declarative DOM framework with CSS selector-based structure and reactive pulsations",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -90,13 +90,24 @@
90
90
  "./compiler/parser": "./compiler/parser.js",
91
91
  "./compiler/transformer": "./compiler/transformer.js",
92
92
  "./compiler/preprocessor": "./compiler/preprocessor.js",
93
+ "./runtime/context": {
94
+ "types": "./types/context.d.ts",
95
+ "default": "./runtime/context.js"
96
+ },
97
+ "./runtime/errors": {
98
+ "types": "./types/errors.d.ts",
99
+ "default": "./runtime/errors.js"
100
+ },
93
101
  "./core/errors": "./runtime/errors.js",
94
102
  "./vite": {
95
103
  "types": "./types/index.d.ts",
96
104
  "default": "./loader/vite-plugin.js"
97
105
  },
106
+ "./vite/server-components": "./loader/vite-plugin-server-components.js",
98
107
  "./webpack": "./loader/webpack-loader.js",
108
+ "./webpack/server-components": "./loader/webpack-loader-server-components.js",
99
109
  "./rollup": "./loader/rollup-plugin.js",
110
+ "./rollup/server-components": "./loader/rollup-plugin-server-components.js",
100
111
  "./esbuild": "./loader/esbuild-plugin.js",
101
112
  "./parcel": "./loader/parcel-plugin.js",
102
113
  "./swc": "./loader/swc-plugin.js",
@@ -105,6 +116,7 @@
105
116
  "./runtime/ssr-stream": "./runtime/ssr-stream.js",
106
117
  "./runtime/ssr-mismatch": "./runtime/ssr-mismatch.js",
107
118
  "./runtime/ssr-preload": "./runtime/ssr-preload.js",
119
+ "./runtime/server-components": "./runtime/server-components/index.js",
108
120
  "./server": "./server/index.js",
109
121
  "./server/express": "./server/express.js",
110
122
  "./server/hono": "./server/hono.js",
@@ -135,7 +147,6 @@
135
147
  "files": [
136
148
  "index.js",
137
149
  "cli/",
138
- "core/",
139
150
  "runtime/",
140
151
  "compiler/",
141
152
  "loader/",
@@ -147,108 +158,139 @@
147
158
  "LICENSE"
148
159
  ],
149
160
  "scripts": {
150
- "test": "node scripts/run-all-tests.js",
161
+ "bench": "node benchmarks/index.js",
162
+ "bench:json": "node benchmarks/index.js --json",
163
+ "build:netlify": "node scripts/build-netlify.js",
164
+ "docs": "node cli/index.js dev docs",
165
+ "test": "node scripts/sync-tests.js --fix && node scripts/run-all-tests.js",
166
+ "test:a11y": "node test/a11y.test.js",
167
+ "test:a11y-enhanced": "node test/a11y-enhanced.test.js",
168
+ "test:a11y-focus-coverage-boost": "node --test test/a11y-focus-coverage-boost.test.js",
169
+ "test:a11y-widgets-coverage-boost": "node --test test/a11y-widgets-coverage-boost.test.js",
170
+ "test:analyze": "node test/analyze.test.js",
171
+ "test:animation": "node --test test/animation.test.js",
172
+ "test:async": "node test/async.test.js",
173
+ "test:async-coverage": "node test/async-coverage.test.js",
174
+ "test:benchmarks": "node test/benchmarks.test.js",
175
+ "test:build": "node test/build.test.js",
176
+ "test:build-extended": "node --test test/build-extended.test.js",
177
+ "test:cli": "node test/cli.test.js",
178
+ "test:cli-create": "node test/cli-create.test.js",
179
+ "test:cli-help": "node --test test/cli-help.test.js",
180
+ "test:cli-logger": "node --test test/cli-logger.test.js",
181
+ "test:cli-mobile": "node --test test/cli-mobile.test.js",
182
+ "test:cli-release": "node --test test/cli-release.test.js",
183
+ "test:cli-ui": "node test/cli-ui.test.js",
151
184
  "test:compiler": "node test/compiler.test.js",
152
- "test:parser-coverage": "node test/parser-coverage.test.js",
153
- "test:sourcemap": "node test/sourcemap.test.js",
154
- "test:sourcemap-coverage-boost": "node --test test/sourcemap-coverage-boost.test.js",
185
+ "test:context": "node test/context.test.js",
186
+ "test:context-stress": "node test/context-stress.test.js",
155
187
  "test:css-parsing": "node test/css-parsing.test.js",
156
- "test:preprocessor": "node test/preprocessor.test.js",
157
- "test:pulse": "node test/pulse.test.js",
188
+ "test:dev-server": "node --test test/dev-server.test.js",
189
+ "test:devtools": "node test/devtools.test.js",
190
+ "test:docs": "node test/docs.test.js",
191
+ "test:docs-nav": "node test/docs-navigation.test.js",
192
+ "test:docs-navigation": "node test/docs-navigation.test.js",
193
+ "test:doctor": "node test/doctor.test.js",
158
194
  "test:dom": "node test/dom.test.js",
195
+ "test:dom-adapter": "node test/dom-adapter.test.js",
196
+ "test:dom-advanced": "node test/dom-advanced.test.js",
197
+ "test:dom-binding": "node test/dom-binding.test.js",
198
+ "test:dom-conditional": "node test/dom-conditional.test.js",
159
199
  "test:dom-element": "node test/dom-element.test.js",
160
200
  "test:dom-element-coverage-boost": "node --test test/dom-element-coverage-boost.test.js",
161
- "test:dom-list": "node test/dom-list.test.js",
162
- "test:dom-conditional": "node test/dom-conditional.test.js",
201
+ "test:dom-event-delegate": "node test/dom-event-delegate.test.js",
163
202
  "test:dom-lifecycle": "node test/dom-lifecycle.test.js",
203
+ "test:dom-list": "node test/dom-list.test.js",
204
+ "test:dom-recycle": "node test/dom-recycle.test.js",
164
205
  "test:dom-selector": "node test/dom-selector.test.js",
165
- "test:dom-adapter": "node test/dom-adapter.test.js",
166
- "test:dom-advanced": "node test/dom-advanced.test.js",
206
+ "test:dom-virtual-list": "node test/dom-virtual-list.test.js",
167
207
  "test:enhanced-mock-adapter": "node test/enhanced-mock-adapter.test.js",
168
- "test:router": "node test/router.test.js",
169
- "test:store": "node test/store.test.js",
170
- "test:context": "node test/context.test.js",
171
- "test:hmr": "node test/hmr.test.js",
172
- "test:hmr-coverage-boost": "node --test test/hmr-coverage-boost.test.js",
173
- "test:lint": "node test/lint.test.js",
174
- "test:format": "node test/format.test.js",
175
- "test:analyze": "node test/analyze.test.js",
176
- "test:cli": "node test/cli.test.js",
177
- "test:cli-ui": "node test/cli-ui.test.js",
178
- "test:cli-create": "node test/cli-create.test.js",
179
- "test:lru-cache": "node test/lru-cache.test.js",
180
- "test:utils": "node test/utils.test.js",
181
- "test:utils-coverage": "node test/utils-coverage.test.js",
182
- "test:docs": "node test/docs.test.js",
183
- "test:docs-nav": "node test/docs-navigation.test.js",
184
- "test:async": "node test/async.test.js",
185
- "test:async-coverage": "node test/async-coverage.test.js",
208
+ "test:error-sanitizer": "node --test test/error-sanitizer.test.js",
209
+ "test:errors": "node test/errors.test.js",
210
+ "test:esbuild-plugin": "node test/esbuild-plugin.test.js",
186
211
  "test:form": "node test/form.test.js",
212
+ "test:form-coverage": "node test/form-coverage.test.js",
213
+ "test:form-edge-cases": "node test/form-edge-cases.test.js",
187
214
  "test:form-v2": "node test/form-v2.test.js",
188
- "test:http": "node test/http.test.js",
189
- "test:devtools": "node test/devtools.test.js",
190
- "test:native": "node test/native.test.js",
191
- "test:native-coverage-boost": "node --test test/native-coverage-boost.test.js",
192
- "test:a11y": "node test/a11y.test.js",
193
- "test:a11y-enhanced": "node test/a11y-enhanced.test.js",
194
- "test:a11y-widgets-coverage-boost": "node --test test/a11y-widgets-coverage-boost.test.js",
195
- "test:a11y-focus-coverage-boost": "node --test test/a11y-focus-coverage-boost.test.js",
196
- "test:logger": "node test/logger.test.js",
197
- "test:logger-prod": "node test/logger-prod.test.js",
198
- "test:logger-coverage-boost": "node --test test/logger-coverage-boost.test.js",
199
- "test:errors": "node test/errors.test.js",
200
- "test:security": "node test/security.test.js",
201
- "test:security-coverage-boost": "node --test test/security-coverage-boost.test.js",
202
- "test:websocket": "node test/websocket.test.js",
203
- "test:websocket-coverage-boost": "node --test test/websocket-coverage-boost.test.js",
215
+ "test:format": "node test/format.test.js",
204
216
  "test:graphql": "node test/graphql.test.js",
205
217
  "test:graphql-coverage": "node test/graphql-coverage.test.js",
206
- "test:doctor": "node test/doctor.test.js",
207
- "test:scaffold": "node test/scaffold.test.js",
208
- "test:test-runner": "node test/test-runner.test.js",
209
- "test:build": "node test/build.test.js",
210
- "test:integration": "node test/integration.test.js",
211
- "test:context-stress": "node test/context-stress.test.js",
212
- "test:form-edge-cases": "node test/form-edge-cases.test.js",
213
- "test:form-coverage": "node test/form-coverage.test.js",
214
218
  "test:graphql-subscriptions": "node test/graphql-subscriptions.test.js",
215
219
  "test:graphql-subscriptions-coverage-boost": "node --test test/graphql-subscriptions-coverage-boost.test.js",
220
+ "test:hmr": "node test/hmr.test.js",
221
+ "test:hmr-coverage-boost": "node --test test/hmr-coverage-boost.test.js",
222
+ "test:http": "node test/http.test.js",
216
223
  "test:http-edge-cases": "node test/http-edge-cases.test.js",
224
+ "test:i18n": "node --test test/i18n.test.js",
225
+ "test:integration": "node test/integration.test.js",
217
226
  "test:integration-advanced": "node test/integration-advanced.test.js",
218
- "test:websocket-stress": "node test/websocket-stress.test.js",
219
- "test:ssr": "node test/ssr.test.js",
220
- "test:ssr-hydrator": "node test/ssr-hydrator.test.js",
221
- "test:webpack-loader": "node test/webpack-loader.test.js",
222
- "test:rollup-plugin": "node test/rollup-plugin.test.js",
223
- "test:esbuild-plugin": "node test/esbuild-plugin.test.js",
224
- "test:parcel-plugin": "node test/parcel-plugin.test.js",
225
- "test:swc-plugin": "node test/swc-plugin.test.js",
226
- "test:dom-recycle": "node test/dom-recycle.test.js",
227
- "test:dom-virtual-list": "node test/dom-virtual-list.test.js",
228
- "test:dom-event-delegate": "node test/dom-event-delegate.test.js",
229
- "test:dom-binding": "node test/dom-binding.test.js",
230
227
  "test:interceptor-manager": "node test/interceptor-manager.test.js",
231
- "test:vite-plugin": "node --test test/vite-plugin.test.js",
228
+ "test:lint": "node test/lint.test.js",
229
+ "test:lite": "node test/lite.test.js",
230
+ "test:logger": "node test/logger.test.js",
231
+ "test:logger-coverage-boost": "node --test test/logger-coverage-boost.test.js",
232
+ "test:logger-prod": "node test/logger-prod.test.js",
233
+ "test:lru-cache": "node test/lru-cache.test.js",
232
234
  "test:memory-cleanup": "node --test test/memory-cleanup.test.js",
233
- "test:sse": "node --test test/sse.test.js",
235
+ "test:mutex": "node --test test/mutex.test.js",
236
+ "test:native": "node test/native.test.js",
237
+ "test:native-coverage-boost": "node --test test/native-coverage-boost.test.js",
238
+ "test:parcel-plugin": "node test/parcel-plugin.test.js",
239
+ "test:path-sanitizer": "node --test test/path-sanitizer.test.js",
240
+ "test:parser-coverage": "node test/parser-coverage.test.js",
234
241
  "test:persistence": "node --test test/persistence.test.js",
235
242
  "test:persistence-coverage-boost": "node --test test/persistence-coverage-boost.test.js",
236
- "test:i18n": "node --test test/i18n.test.js",
237
243
  "test:portal": "node --test test/portal.test.js",
238
- "test:animation": "node --test test/animation.test.js",
239
- "test:sw": "node --test test/sw.test.js",
240
- "test:dev-server": "node --test test/dev-server.test.js",
241
- "test:ssr-stream": "node --test test/ssr-stream.test.js",
244
+ "test:preprocessor": "node test/preprocessor.test.js",
245
+ "test:pulse": "node test/pulse.test.js",
246
+ "test:rollup-plugin": "node test/rollup-plugin.test.js",
247
+ "test:router": "node test/router.test.js",
248
+ "test:router-psc": "node --test test/router-psc-integration.test.js",
249
+ "test:router-psc-integration": "node test/router-psc-integration.test.js",
250
+ "test:scaffold": "node test/scaffold.test.js",
251
+ "test:security": "node test/security.test.js",
252
+ "test:security-coverage-boost": "node --test test/security-coverage-boost.test.js",
253
+ "test:security-regression": "node test/security-regression.test.js",
254
+ "test:server-utils": "node --test test/server-utils.test.js",
255
+ "test:server-actions": "node --test test/server-actions.test.js",
256
+ "test:server-actions-client": "node --test test/server-actions-client.test.js",
257
+ "test:server-actions-server-extended": "node --test test/server-actions-server-extended.test.js",
258
+ "test:server-adapters": "node --test test/server-adapters.test.js",
259
+ "test:server-components-build-tools": "node --test test/server-components-build-tools.test.js",
260
+ "test:server-components-client": "node --test test/server-components-client.test.js",
261
+ "test:server-components-compiler": "node --test test/server-components-compiler.test.js",
262
+ "test:server-components-core": "node --test test/server-components-core.test.js",
263
+ "test:server-components-csrf": "node --test test/server-components-csrf.test.js",
264
+ "test:server-components-ratelimit": "node --test test/server-components-ratelimit.test.js",
265
+ "test:server-components-ratelimit-extended": "node --test test/server-components-ratelimit-extended.test.js",
266
+ "test:server-components-security": "node --test test/server-components-security.test.js",
267
+ "test:server-components-security-comprehensive": "node --test test/server-components-security-comprehensive.test.js",
268
+ "test:server-components-serializer": "node --test test/server-components-serializer.test.js",
269
+ "test:server-components-validation": "node --test test/server-components-validation.test.js",
270
+ "test:sourcemap": "node test/sourcemap.test.js",
271
+ "test:sourcemap-coverage-boost": "node --test test/sourcemap-coverage-boost.test.js",
272
+ "test:sse": "node --test test/sse.test.js",
273
+ "test:ssg": "node --test test/ssg.test.js",
274
+ "test:ssr": "node test/ssr.test.js",
275
+ "test:ssr-directives": "node --test test/ssr-directives.test.js",
276
+ "test:ssr-hydrator": "node test/ssr-hydrator.test.js",
242
277
  "test:ssr-mismatch": "node --test test/ssr-mismatch.test.js",
243
278
  "test:ssr-preload": "node --test test/ssr-preload.test.js",
244
- "test:ssr-directives": "node --test test/ssr-directives.test.js",
245
- "test:ssg": "node --test test/ssg.test.js",
246
- "test:server-adapters": "node --test test/server-adapters.test.js",
247
- "bench": "node benchmarks/index.js",
248
- "bench:json": "node benchmarks/index.js --json",
249
- "build:netlify": "node scripts/build-netlify.js",
250
- "version": "node scripts/sync-version.js",
251
- "docs": "node cli/index.js dev docs"
279
+ "test:ssr-stream": "node --test test/ssr-stream.test.js",
280
+ "test:store": "node test/store.test.js",
281
+ "test:sw": "node --test test/sw.test.js",
282
+ "test:swc-plugin": "node test/swc-plugin.test.js",
283
+ "test:sync": "node scripts/sync-tests.js",
284
+ "test:sync:fix": "node scripts/sync-tests.js --fix",
285
+ "test:test-runner": "node test/test-runner.test.js",
286
+ "test:utils": "node test/utils.test.js",
287
+ "test:utils-coverage": "node test/utils-coverage.test.js",
288
+ "test:vite-plugin": "node --test test/vite-plugin.test.js",
289
+ "test:webpack-loader": "node test/webpack-loader.test.js",
290
+ "test:websocket": "node test/websocket.test.js",
291
+ "test:websocket-coverage-boost": "node --test test/websocket-coverage-boost.test.js",
292
+ "test:websocket-stress": "node test/websocket-stress.test.js",
293
+ "version": "node scripts/sync-version.js"
252
294
  },
253
295
  "keywords": [
254
296
  "framework",
@@ -275,7 +317,7 @@
275
317
  "url": "https://github.com/vincenthirtz/pulse-js-framework/issues"
276
318
  },
277
319
  "engines": {
278
- "node": ">=18.0.0"
320
+ "node": ">=20.0.0"
279
321
  },
280
322
  "dependencies": {},
281
323
  "devDependencies": {}
package/runtime/async.js CHANGED
@@ -795,6 +795,10 @@ export function usePolling(asyncFn, options) {
795
795
  onError
796
796
  } = options;
797
797
 
798
+ if (!interval || typeof interval !== 'number' || interval <= 0) {
799
+ throw new Error('usePolling: options.interval must be a positive number (milliseconds)');
800
+ }
801
+
798
802
  const data = pulse(null);
799
803
  const error = pulse(null);
800
804
  const isPolling = pulse(false);