pulse-js-framework 1.7.11 → 1.7.12

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 CHANGED
@@ -1,9 +1,10 @@
1
1
  # Pulse Framework
2
2
 
3
3
  [![CI](https://github.com/vincenthirtz/pulse-js-framework/actions/workflows/ci.yml/badge.svg)](https://github.com/vincenthirtz/pulse-js-framework/actions/workflows/ci.yml)
4
+ [![codecov](https://codecov.io/gh/vincenthirtz/pulse-js-framework/graph/badge.svg)](https://codecov.io/gh/vincenthirtz/pulse-js-framework)
4
5
  [![Netlify Status](https://api.netlify.com/api/v1/badges/2597dac2-228a-4d3e-bea8-4e7ef8ac5c53/deploy-status)](https://app.netlify.com/projects/pulse-js/deploys)
5
6
 
6
- A declarative DOM framework with CSS selector-based structure and reactive pulsations.
7
+ No build. No dependencies. Just JavaScript.
7
8
 
8
9
  ## Features
9
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pulse-js-framework",
3
- "version": "1.7.11",
3
+ "version": "1.7.12",
4
4
  "description": "A declarative DOM framework with CSS selector-based structure and reactive pulsations",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -73,6 +73,14 @@
73
73
  "default": "./runtime/a11y.js"
74
74
  },
75
75
  "./runtime/devtools": "./runtime/devtools.js",
76
+ "./runtime/websocket": {
77
+ "types": "./types/websocket.d.ts",
78
+ "default": "./runtime/websocket.js"
79
+ },
80
+ "./runtime/graphql": {
81
+ "types": "./types/graphql.d.ts",
82
+ "default": "./runtime/graphql.js"
83
+ },
76
84
  "./compiler": {
77
85
  "types": "./types/index.d.ts",
78
86
  "default": "./compiler/index.js"
@@ -101,7 +109,7 @@
101
109
  "LICENSE"
102
110
  ],
103
111
  "scripts": {
104
- "test": "npm run test:compiler && npm run test:sourcemap && npm run test:pulse && npm run test:dom && npm run test:dom-adapter && npm run test:router && npm run test:store && npm run test:hmr && npm run test:lint && npm run test:format && npm run test:analyze && npm run test:lru-cache && npm run test:utils && npm run test:docs && npm run test:async && npm run test:form && npm run test:http && npm run test:devtools && npm run test:native && npm run test:a11y && npm run test:logger && npm run test:errors",
112
+ "test": "npm run test:compiler && npm run test:sourcemap && npm run test:pulse && npm run test:dom && npm run test:dom-adapter && npm run test:router && npm run test:store && npm run test:hmr && npm run test:lint && npm run test:format && npm run test:analyze && npm run test:cli && npm run test:cli-ui && npm run test:lru-cache && npm run test:utils && npm run test:docs && npm run test:async && npm run test:form && npm run test:http && npm run test:devtools && npm run test:native && npm run test:a11y && npm run test:logger && npm run test:errors && npm run test:security && npm run test:websocket && npm run test:graphql",
105
113
  "test:compiler": "node test/compiler.test.js",
106
114
  "test:sourcemap": "node test/sourcemap.test.js",
107
115
  "test:pulse": "node test/pulse.test.js",
@@ -113,6 +121,8 @@
113
121
  "test:lint": "node test/lint.test.js",
114
122
  "test:format": "node test/format.test.js",
115
123
  "test:analyze": "node test/analyze.test.js",
124
+ "test:cli": "node test/cli.test.js",
125
+ "test:cli-ui": "node test/cli-ui.test.js",
116
126
  "test:lru-cache": "node test/lru-cache.test.js",
117
127
  "test:utils": "node test/utils.test.js",
118
128
  "test:docs": "node test/docs.test.js",
@@ -124,6 +134,9 @@
124
134
  "test:a11y": "node test/a11y.test.js",
125
135
  "test:logger": "node test/logger.test.js",
126
136
  "test:errors": "node test/errors.test.js",
137
+ "test:security": "node test/security.test.js",
138
+ "test:websocket": "node test/websocket.test.js",
139
+ "test:graphql": "node test/graphql.test.js",
127
140
  "build:netlify": "node scripts/build-netlify.js",
128
141
  "version": "node scripts/sync-version.js",
129
142
  "docs": "node cli/index.js dev docs"
@@ -0,0 +1,374 @@
1
+ /**
2
+ * Pulse Context API - Dependency Injection and Prop Drilling Prevention
3
+ * @module pulse-js-framework/runtime/context
4
+ *
5
+ * Provides a React-like Context API for passing data through the component tree
6
+ * without explicit prop passing at every level.
7
+ *
8
+ * @example
9
+ * import { createContext, useContext, Provider } from './context.js';
10
+ * import { pulse } from './pulse.js';
11
+ *
12
+ * // Create a context with default value
13
+ * const ThemeContext = createContext('light');
14
+ *
15
+ * // Provide a value to the subtree
16
+ * const App = () => {
17
+ * const theme = pulse('dark');
18
+ * return Provider(ThemeContext, theme, () => [
19
+ * Header(),
20
+ * Content()
21
+ * ]);
22
+ * };
23
+ *
24
+ * // Consume the context anywhere in the subtree
25
+ * const Button = () => {
26
+ * const theme = useContext(ThemeContext);
27
+ * return el(`button.btn-${theme.get()}`);
28
+ * };
29
+ */
30
+
31
+ import { pulse, effect, computed, Pulse } from './pulse.js';
32
+
33
+ /**
34
+ * Check if a value is a Pulse instance
35
+ * @param {any} value - Value to check
36
+ * @returns {boolean} True if value is a Pulse
37
+ */
38
+ function isPulse(value) {
39
+ return value instanceof Pulse;
40
+ }
41
+ import { loggers } from './logger.js';
42
+ import { PulseError } from './errors.js';
43
+
44
+ const log = loggers.pulse;
45
+
46
+ // =============================================================================
47
+ // CONTEXT REGISTRY
48
+ // =============================================================================
49
+
50
+ /**
51
+ * Unique ID counter for contexts
52
+ * @type {number}
53
+ */
54
+ let contextIdCounter = 0;
55
+
56
+ /**
57
+ * Context stack for provider nesting
58
+ * Each context has its own stack of provided values
59
+ * @type {Map<symbol, Array<any>>}
60
+ */
61
+ const contextStacks = new Map();
62
+
63
+ /**
64
+ * Map of context IDs to their default values
65
+ * @type {Map<symbol, any>}
66
+ */
67
+ const contextDefaults = new Map();
68
+
69
+ /**
70
+ * Map of context IDs to their display names (for debugging)
71
+ * @type {Map<symbol, string>}
72
+ */
73
+ const contextNames = new Map();
74
+
75
+ // =============================================================================
76
+ // CONTEXT CREATION
77
+ // =============================================================================
78
+
79
+ /**
80
+ * @typedef {Object} Context
81
+ * @property {symbol} _id - Unique identifier for this context
82
+ * @property {string} displayName - Human-readable name for debugging
83
+ * @property {any} defaultValue - Default value when no provider is found
84
+ */
85
+
86
+ /**
87
+ * Create a new context with a default value
88
+ *
89
+ * @template T
90
+ * @param {T} defaultValue - Value used when no Provider is found in the tree
91
+ * @param {Object} [options={}] - Context options
92
+ * @param {string} [options.displayName] - Name for debugging purposes
93
+ * @returns {Context<T>} Context object to pass to Provider and useContext
94
+ *
95
+ * @example
96
+ * // Create a theme context
97
+ * const ThemeContext = createContext('light');
98
+ *
99
+ * // Create a user context with object default
100
+ * const UserContext = createContext({ name: 'Guest', role: 'anonymous' });
101
+ *
102
+ * // Create with display name for debugging
103
+ * const AuthContext = createContext(null, { displayName: 'AuthContext' });
104
+ */
105
+ export function createContext(defaultValue, options = {}) {
106
+ const id = Symbol(`pulse.context.${++contextIdCounter}`);
107
+ const displayName = options.displayName || `Context${contextIdCounter}`;
108
+
109
+ // Initialize the context stack
110
+ contextStacks.set(id, []);
111
+ contextDefaults.set(id, defaultValue);
112
+ contextNames.set(id, displayName);
113
+
114
+ const context = {
115
+ _id: id,
116
+ displayName,
117
+ defaultValue,
118
+
119
+ // Provider shorthand method
120
+ Provider: (value, children) => Provider(context, value, children),
121
+
122
+ // Consumer shorthand method
123
+ Consumer: (render) => Consumer(context, render)
124
+ };
125
+
126
+ Object.freeze(context);
127
+ return context;
128
+ }
129
+
130
+ // =============================================================================
131
+ // CONTEXT PROVIDER
132
+ // =============================================================================
133
+
134
+ /**
135
+ * Provide a value to a context for all descendants
136
+ *
137
+ * @template T
138
+ * @param {Context<T>} context - Context object from createContext
139
+ * @param {T|Pulse<T>} value - Value to provide (can be reactive pulse)
140
+ * @param {Function|Array} children - Child elements or render function
141
+ * @returns {Node|Array<Node>} The rendered children
142
+ *
143
+ * @example
144
+ * // Provide a static value
145
+ * Provider(ThemeContext, 'dark', () => [
146
+ * Header(),
147
+ * Content()
148
+ * ]);
149
+ *
150
+ * // Provide a reactive value
151
+ * const theme = pulse('dark');
152
+ * Provider(ThemeContext, theme, () => [
153
+ * Header(),
154
+ * Content()
155
+ * ]);
156
+ *
157
+ * // Using context.Provider shorthand
158
+ * ThemeContext.Provider('dark', () => App());
159
+ */
160
+ export function Provider(context, value, children) {
161
+ if (!context || !context._id) {
162
+ throw new PulseError('Provider requires a valid context created with createContext()', {
163
+ code: 'INVALID_CONTEXT'
164
+ });
165
+ }
166
+
167
+ const stack = contextStacks.get(context._id);
168
+ if (!stack) {
169
+ throw new PulseError(`Context "${context.displayName}" has been disposed`, {
170
+ code: 'CONTEXT_DISPOSED'
171
+ });
172
+ }
173
+
174
+ // Wrap non-pulse values in a pulse for consistent reactive API
175
+ const reactiveValue = isPulse(value) ? value : pulse(value);
176
+
177
+ // Push value onto context stack
178
+ stack.push(reactiveValue);
179
+ log.debug(`Provider: pushed value to ${context.displayName}, depth=${stack.length}`);
180
+
181
+ let result;
182
+ try {
183
+ // Render children with the new context value available
184
+ result = typeof children === 'function' ? children() : children;
185
+ } finally {
186
+ // Pop value from stack when provider scope ends
187
+ stack.pop();
188
+ log.debug(`Provider: popped value from ${context.displayName}, depth=${stack.length}`);
189
+ }
190
+
191
+ return result;
192
+ }
193
+
194
+ // =============================================================================
195
+ // CONTEXT CONSUMER
196
+ // =============================================================================
197
+
198
+ /**
199
+ * Get the current value from a context
200
+ *
201
+ * Returns a reactive pulse that updates when the provided value changes.
202
+ * If no Provider is found, returns the context's default value.
203
+ *
204
+ * @template T
205
+ * @param {Context<T>} context - Context object from createContext
206
+ * @returns {Pulse<T>} Reactive pulse containing the context value
207
+ *
208
+ * @example
209
+ * const theme = useContext(ThemeContext);
210
+ * console.log(theme.get()); // 'dark' (or default if no provider)
211
+ *
212
+ * // Reactive usage in effects
213
+ * effect(() => {
214
+ * document.body.className = theme.get();
215
+ * });
216
+ */
217
+ export function useContext(context) {
218
+ if (!context || !context._id) {
219
+ throw new PulseError('useContext requires a valid context created with createContext()', {
220
+ code: 'INVALID_CONTEXT'
221
+ });
222
+ }
223
+
224
+ const stack = contextStacks.get(context._id);
225
+ if (!stack) {
226
+ throw new PulseError(`Context "${context.displayName}" has been disposed`, {
227
+ code: 'CONTEXT_DISPOSED'
228
+ });
229
+ }
230
+
231
+ // Get the current provider value (top of stack) or default
232
+ if (stack.length > 0) {
233
+ const value = stack[stack.length - 1];
234
+ log.debug(`useContext: got value from ${context.displayName}, depth=${stack.length}`);
235
+ return value;
236
+ }
237
+
238
+ // No provider found, return default as reactive pulse
239
+ log.debug(`useContext: using default for ${context.displayName}`);
240
+ const defaultVal = contextDefaults.get(context._id);
241
+
242
+ // Wrap default in pulse for consistent API
243
+ return isPulse(defaultVal) ? defaultVal : pulse(defaultVal);
244
+ }
245
+
246
+ /**
247
+ * Consumer component pattern for context consumption
248
+ *
249
+ * @template T
250
+ * @param {Context<T>} context - Context object from createContext
251
+ * @param {Function} render - Render function receiving the context value
252
+ * @returns {any} Result of the render function
253
+ *
254
+ * @example
255
+ * Consumer(ThemeContext, (theme) => {
256
+ * return el(`button.btn-${theme.get()}`, 'Click me');
257
+ * });
258
+ *
259
+ * // Using shorthand
260
+ * ThemeContext.Consumer((theme) => el('span', theme.get()));
261
+ */
262
+ export function Consumer(context, render) {
263
+ const value = useContext(context);
264
+ return render(value);
265
+ }
266
+
267
+ // =============================================================================
268
+ // CONTEXT UTILITIES
269
+ // =============================================================================
270
+
271
+ /**
272
+ * Check if a value is a valid context object
273
+ *
274
+ * @param {any} value - Value to check
275
+ * @returns {boolean} True if value is a valid context
276
+ */
277
+ export function isContext(value) {
278
+ return value !== null &&
279
+ typeof value === 'object' &&
280
+ typeof value._id === 'symbol' &&
281
+ contextStacks.has(value._id);
282
+ }
283
+
284
+ /**
285
+ * Get the current provider depth for a context (useful for debugging)
286
+ *
287
+ * @param {Context} context - Context to check
288
+ * @returns {number} Current nesting depth of providers
289
+ */
290
+ export function getContextDepth(context) {
291
+ if (!context || !context._id) return 0;
292
+ const stack = contextStacks.get(context._id);
293
+ return stack ? stack.length : 0;
294
+ }
295
+
296
+ /**
297
+ * Dispose a context and clean up its resources
298
+ * Should be called when a context is no longer needed (e.g., in tests)
299
+ *
300
+ * @param {Context} context - Context to dispose
301
+ */
302
+ export function disposeContext(context) {
303
+ if (!context || !context._id) return;
304
+
305
+ contextStacks.delete(context._id);
306
+ contextDefaults.delete(context._id);
307
+ contextNames.delete(context._id);
308
+
309
+ log.debug(`Context disposed: ${context.displayName}`);
310
+ }
311
+
312
+ /**
313
+ * Create a derived context value from one or more contexts
314
+ *
315
+ * @template T
316
+ * @param {Function} selector - Function that receives context values and returns derived value
317
+ * @param {...Context} contexts - Contexts to derive from
318
+ * @returns {Pulse<T>} Computed pulse with derived value
319
+ *
320
+ * @example
321
+ * const theme = useContextSelector(
322
+ * (settings, user) => settings.get().theme || user.get().preferredTheme,
323
+ * SettingsContext,
324
+ * UserContext
325
+ * );
326
+ */
327
+ export function useContextSelector(selector, ...contexts) {
328
+ const values = contexts.map(ctx => useContext(ctx));
329
+
330
+ return computed(() => {
331
+ return selector(...values);
332
+ });
333
+ }
334
+
335
+ /**
336
+ * Provide multiple contexts at once
337
+ *
338
+ * @param {Array<[Context, any]>} providers - Array of [context, value] pairs
339
+ * @param {Function} children - Render function for children
340
+ * @returns {any} Rendered children
341
+ *
342
+ * @example
343
+ * provideMany([
344
+ * [ThemeContext, 'dark'],
345
+ * [UserContext, currentUser],
346
+ * [LocaleContext, 'fr']
347
+ * ], () => App());
348
+ */
349
+ export function provideMany(providers, children) {
350
+ if (providers.length === 0) {
351
+ return typeof children === 'function' ? children() : children;
352
+ }
353
+
354
+ const [first, ...rest] = providers;
355
+ const [context, value] = first;
356
+
357
+ return Provider(context, value, () => provideMany(rest, children));
358
+ }
359
+
360
+ // =============================================================================
361
+ // DEFAULT EXPORT
362
+ // =============================================================================
363
+
364
+ export default {
365
+ createContext,
366
+ useContext,
367
+ Provider,
368
+ Consumer,
369
+ isContext,
370
+ getContextDepth,
371
+ disposeContext,
372
+ useContextSelector,
373
+ provideMany
374
+ };