pulse-js-framework 1.10.3 → 1.11.0

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 (66) 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 +127 -74
  21. package/runtime/a11y/widgets.js +14 -1
  22. package/runtime/async.js +4 -0
  23. package/runtime/context.js +16 -3
  24. package/runtime/dom-adapter.js +5 -3
  25. package/runtime/dom-virtual-list.js +2 -1
  26. package/runtime/form.js +8 -3
  27. package/runtime/graphql/cache.js +1 -1
  28. package/runtime/graphql/client.js +22 -0
  29. package/runtime/graphql/hooks.js +12 -6
  30. package/runtime/graphql/subscriptions.js +4 -0
  31. package/runtime/hmr.js +6 -3
  32. package/runtime/http.js +1 -0
  33. package/runtime/i18n.js +2 -0
  34. package/runtime/lru-cache.js +3 -1
  35. package/runtime/native.js +46 -20
  36. package/runtime/pulse.js +3 -0
  37. package/runtime/router/core.js +5 -1
  38. package/runtime/router/index.js +17 -1
  39. package/runtime/router/psc-integration.js +301 -0
  40. package/runtime/security.js +58 -29
  41. package/runtime/server-components/actions-server.js +798 -0
  42. package/runtime/server-components/actions.js +389 -0
  43. package/runtime/server-components/client.js +447 -0
  44. package/runtime/server-components/error-sanitizer.js +438 -0
  45. package/runtime/server-components/index.js +275 -0
  46. package/runtime/server-components/security-csrf.js +593 -0
  47. package/runtime/server-components/security-errors.js +227 -0
  48. package/runtime/server-components/security-ratelimit.js +733 -0
  49. package/runtime/server-components/security-validation.js +467 -0
  50. package/runtime/server-components/security.js +598 -0
  51. package/runtime/server-components/serializer.js +617 -0
  52. package/runtime/server-components/server.js +382 -0
  53. package/runtime/server-components/types.js +383 -0
  54. package/runtime/server-components/utils/mutex.js +60 -0
  55. package/runtime/server-components/utils/path-sanitizer.js +109 -0
  56. package/runtime/ssr.js +2 -1
  57. package/runtime/store.js +19 -10
  58. package/runtime/utils.js +12 -128
  59. package/types/animation.d.ts +300 -0
  60. package/types/i18n.d.ts +283 -0
  61. package/types/persistence.d.ts +267 -0
  62. package/types/sse.d.ts +248 -0
  63. package/types/sw.d.ts +150 -0
  64. package/runtime/a11y.js.original +0 -1844
  65. package/runtime/graphql.js.original +0 -1326
  66. 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.3",
3
+ "version": "1.11.0",
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,97 +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",
185
+ "test:context": "node test/context.test.js",
186
+ "test:context-stress": "node test/context-stress.test.js",
154
187
  "test:css-parsing": "node test/css-parsing.test.js",
155
- "test:preprocessor": "node test/preprocessor.test.js",
156
- "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",
157
194
  "test:dom": "node test/dom.test.js",
158
- "test:dom-element": "node test/dom-element.test.js",
159
- "test:dom-list": "node test/dom-list.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",
160
198
  "test:dom-conditional": "node test/dom-conditional.test.js",
199
+ "test:dom-element": "node test/dom-element.test.js",
200
+ "test:dom-element-coverage-boost": "node --test test/dom-element-coverage-boost.test.js",
201
+ "test:dom-event-delegate": "node test/dom-event-delegate.test.js",
161
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",
162
205
  "test:dom-selector": "node test/dom-selector.test.js",
163
- "test:dom-adapter": "node test/dom-adapter.test.js",
164
- "test:dom-advanced": "node test/dom-advanced.test.js",
206
+ "test:dom-virtual-list": "node test/dom-virtual-list.test.js",
165
207
  "test:enhanced-mock-adapter": "node test/enhanced-mock-adapter.test.js",
166
- "test:router": "node test/router.test.js",
167
- "test:store": "node test/store.test.js",
168
- "test:context": "node test/context.test.js",
169
- "test:hmr": "node test/hmr.test.js",
170
- "test:lint": "node test/lint.test.js",
171
- "test:format": "node test/format.test.js",
172
- "test:analyze": "node test/analyze.test.js",
173
- "test:cli": "node test/cli.test.js",
174
- "test:cli-ui": "node test/cli-ui.test.js",
175
- "test:cli-create": "node test/cli-create.test.js",
176
- "test:lru-cache": "node test/lru-cache.test.js",
177
- "test:utils": "node test/utils.test.js",
178
- "test:utils-coverage": "node test/utils-coverage.test.js",
179
- "test:docs": "node test/docs.test.js",
180
- "test:docs-nav": "node test/docs-navigation.test.js",
181
- "test:async": "node test/async.test.js",
182
- "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",
183
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",
184
214
  "test:form-v2": "node test/form-v2.test.js",
185
- "test:http": "node test/http.test.js",
186
- "test:devtools": "node test/devtools.test.js",
187
- "test:native": "node test/native.test.js",
188
- "test:a11y": "node test/a11y.test.js",
189
- "test:a11y-enhanced": "node test/a11y-enhanced.test.js",
190
- "test:logger": "node test/logger.test.js",
191
- "test:logger-prod": "node test/logger-prod.test.js",
192
- "test:errors": "node test/errors.test.js",
193
- "test:security": "node test/security.test.js",
194
- "test:websocket": "node test/websocket.test.js",
215
+ "test:format": "node test/format.test.js",
195
216
  "test:graphql": "node test/graphql.test.js",
196
217
  "test:graphql-coverage": "node test/graphql-coverage.test.js",
197
- "test:doctor": "node test/doctor.test.js",
198
- "test:scaffold": "node test/scaffold.test.js",
199
- "test:test-runner": "node test/test-runner.test.js",
200
- "test:build": "node test/build.test.js",
201
- "test:integration": "node test/integration.test.js",
202
- "test:context-stress": "node test/context-stress.test.js",
203
- "test:form-edge-cases": "node test/form-edge-cases.test.js",
204
- "test:form-coverage": "node test/form-coverage.test.js",
205
218
  "test:graphql-subscriptions": "node test/graphql-subscriptions.test.js",
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",
206
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",
207
226
  "test:integration-advanced": "node test/integration-advanced.test.js",
208
- "test:websocket-stress": "node test/websocket-stress.test.js",
209
- "test:ssr": "node test/ssr.test.js",
210
- "test:ssr-hydrator": "node test/ssr-hydrator.test.js",
211
- "test:webpack-loader": "node test/webpack-loader.test.js",
212
- "test:rollup-plugin": "node test/rollup-plugin.test.js",
213
- "test:esbuild-plugin": "node test/esbuild-plugin.test.js",
214
- "test:parcel-plugin": "node test/parcel-plugin.test.js",
215
- "test:swc-plugin": "node test/swc-plugin.test.js",
216
- "test:dom-recycle": "node test/dom-recycle.test.js",
217
- "test:dom-virtual-list": "node test/dom-virtual-list.test.js",
218
- "test:dom-event-delegate": "node test/dom-event-delegate.test.js",
219
- "test:dom-binding": "node test/dom-binding.test.js",
220
227
  "test:interceptor-manager": "node test/interceptor-manager.test.js",
221
- "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",
222
234
  "test:memory-cleanup": "node --test test/memory-cleanup.test.js",
223
- "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",
224
241
  "test:persistence": "node --test test/persistence.test.js",
225
- "test:i18n": "node --test test/i18n.test.js",
242
+ "test:persistence-coverage-boost": "node --test test/persistence-coverage-boost.test.js",
226
243
  "test:portal": "node --test test/portal.test.js",
227
- "test:animation": "node --test test/animation.test.js",
228
- "test:sw": "node --test test/sw.test.js",
229
- "test:dev-server": "node --test test/dev-server.test.js",
230
- "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",
231
277
  "test:ssr-mismatch": "node --test test/ssr-mismatch.test.js",
232
278
  "test:ssr-preload": "node --test test/ssr-preload.test.js",
233
- "test:ssr-directives": "node --test test/ssr-directives.test.js",
234
- "test:ssg": "node --test test/ssg.test.js",
235
- "test:server-adapters": "node --test test/server-adapters.test.js",
236
- "bench": "node benchmarks/index.js",
237
- "bench:json": "node benchmarks/index.js --json",
238
- "build:netlify": "node scripts/build-netlify.js",
239
- "version": "node scripts/sync-version.js",
240
- "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"
241
294
  },
242
295
  "keywords": [
243
296
  "framework",
@@ -264,7 +317,7 @@
264
317
  "url": "https://github.com/vincenthirtz/pulse-js-framework/issues"
265
318
  },
266
319
  "engines": {
267
- "node": ">=18.0.0"
320
+ "node": ">=20.0.0"
268
321
  },
269
322
  "dependencies": {},
270
323
  "devDependencies": {}
@@ -445,6 +445,7 @@ export function createMenu(button, menu, options = {}) {
445
445
  const menuId = menu.id || generateId('menu');
446
446
  let rovingCleanup = null;
447
447
  let documentClickHandler = null;
448
+ let documentClickTimeout = null;
448
449
 
449
450
  // Set ARIA attributes
450
451
  menu.id = menuId;
@@ -476,7 +477,8 @@ export function createMenu(button, menu, options = {}) {
476
477
  if (firstItem) firstItem.focus();
477
478
 
478
479
  // Close on click outside (delay to avoid immediate close)
479
- setTimeout(() => {
480
+ documentClickTimeout = setTimeout(() => {
481
+ documentClickTimeout = null;
480
482
  documentClickHandler = (e) => {
481
483
  if (!button.contains(e.target) && !menu.contains(e.target)) {
482
484
  close();
@@ -498,6 +500,11 @@ export function createMenu(button, menu, options = {}) {
498
500
  rovingCleanup = null;
499
501
  }
500
502
 
503
+ if (documentClickTimeout) {
504
+ clearTimeout(documentClickTimeout);
505
+ documentClickTimeout = null;
506
+ }
507
+
501
508
  if (documentClickHandler) {
502
509
  document.removeEventListener('click', documentClickHandler);
503
510
  documentClickHandler = null;
@@ -533,11 +540,17 @@ export function createMenu(button, menu, options = {}) {
533
540
  button.removeEventListener('click', toggle);
534
541
  button.removeEventListener('keydown', handleButtonKeyDown);
535
542
  menu.removeEventListener('keydown', handleMenuKeyDown);
543
+ if (documentClickTimeout) {
544
+ clearTimeout(documentClickTimeout);
545
+ documentClickTimeout = null;
546
+ }
536
547
  if (documentClickHandler) {
537
548
  document.removeEventListener('click', documentClickHandler);
549
+ documentClickHandler = null;
538
550
  }
539
551
  if (rovingCleanup) {
540
552
  rovingCleanup();
553
+ rovingCleanup = null;
541
554
  }
542
555
  };
543
556