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 +2 -1
- package/package.json +15 -2
- package/runtime/context.js +374 -0
- package/runtime/graphql.js +1356 -0
- package/runtime/index.js +6 -0
- package/runtime/logger.js +2 -1
- package/runtime/websocket.js +874 -0
- package/types/context.d.ts +171 -0
- package/types/graphql.d.ts +490 -0
- package/types/index.d.ts +15 -0
- package/types/websocket.d.ts +347 -0
package/README.md
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# Pulse Framework
|
|
2
2
|
|
|
3
3
|
[](https://github.com/vincenthirtz/pulse-js-framework/actions/workflows/ci.yml)
|
|
4
|
+
[](https://codecov.io/gh/vincenthirtz/pulse-js-framework)
|
|
4
5
|
[](https://app.netlify.com/projects/pulse-js/deploys)
|
|
5
6
|
|
|
6
|
-
|
|
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.
|
|
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
|
+
};
|