pulse-js-framework 1.7.30 → 1.7.31

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/cli/index.js CHANGED
@@ -15,8 +15,14 @@ const __filename = fileURLToPath(import.meta.url);
15
15
  const __dirname = dirname(__filename);
16
16
 
17
17
  // Version - read dynamically from package.json
18
- const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
19
- const VERSION = pkg.version;
18
+ let VERSION = '0.0.0';
19
+ try {
20
+ const pkgContent = readFileSync(join(__dirname, '..', 'package.json'), 'utf-8');
21
+ const pkg = JSON.parse(pkgContent);
22
+ VERSION = pkg.version || VERSION;
23
+ } catch (err) {
24
+ log.warn(`Could not read package.json: ${err.message}`);
25
+ }
20
26
 
21
27
  // Available example templates
22
28
  const TEMPLATES = {
@@ -683,10 +689,19 @@ async function initProject(args) {
683
689
 
684
690
  if (existsSync(pkgPath)) {
685
691
  try {
686
- pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
687
- log.info('Found existing package.json, merging...');
692
+ const pkgContent = readFileSync(pkgPath, 'utf-8');
693
+ if (!pkgContent.trim()) {
694
+ log.warn('Existing package.json is empty, creating new one.');
695
+ } else {
696
+ pkg = JSON.parse(pkgContent);
697
+ log.info('Found existing package.json, merging...');
698
+ }
688
699
  } catch (e) {
689
- log.warn('Could not parse existing package.json, creating new one.');
700
+ if (e instanceof SyntaxError) {
701
+ log.warn(`Invalid JSON in package.json: ${e.message}. Creating new one.`);
702
+ } else {
703
+ log.warn(`Could not read package.json: ${e.message}. Creating new one.`);
704
+ }
690
705
  }
691
706
  }
692
707
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pulse-js-framework",
3
- "version": "1.7.30",
3
+ "version": "1.7.31",
4
4
  "description": "A declarative DOM framework with CSS selector-based structure and reactive pulsations",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -109,14 +109,19 @@
109
109
  "LICENSE"
110
110
  ],
111
111
  "scripts": {
112
- "test": "npm run test:compiler && npm run test:sourcemap && npm run test:css-parsing && npm run test:pulse && npm run test:dom && npm run test:dom-element && npm run test:dom-adapter && npm run test:enhanced-mock-adapter && npm run test:router && npm run test:store && npm run test:context && 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:cli-create && npm run test:lru-cache && npm run test:utils && npm run test:docs && npm run test:docs-nav && 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:a11y-enhanced && npm run test:logger && npm run test:errors && npm run test:security && npm run test:websocket && npm run test:graphql && npm run test:doctor && npm run test:scaffold && npm run test:test-runner && npm run test:build && npm run test:integration && npm run test:context-stress && npm run test:form-edge-cases && npm run test:graphql-subscriptions && npm run test:http-edge-cases && npm run test:integration-advanced && npm run test:websocket-stress && npm run test:ssr",
112
+ "test": "npm run test:compiler && npm run test:sourcemap && npm run test:css-parsing && npm run test:pulse && npm run test:dom && npm run test:dom-element && npm run test:dom-list && npm run test:dom-conditional && npm run test:dom-lifecycle && npm run test:dom-selector && npm run test:dom-adapter && npm run test:dom-advanced && npm run test:enhanced-mock-adapter && npm run test:router && npm run test:store && npm run test:context && 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:cli-create && npm run test:lru-cache && npm run test:utils && npm run test:utils-coverage && npm run test:docs && npm run test:docs-nav && npm run test:async && npm run test:async-coverage && npm run test:form && npm run test:http && npm run test:devtools && npm run test:native && npm run test:a11y && npm run test:a11y-enhanced && npm run test:logger && npm run test:logger-prod && npm run test:errors && npm run test:security && npm run test:websocket && npm run test:graphql && npm run test:graphql-coverage && npm run test:doctor && npm run test:scaffold && npm run test:test-runner && npm run test:build && npm run test:integration && npm run test:context-stress && npm run test:form-edge-cases && npm run test:graphql-subscriptions && npm run test:http-edge-cases && npm run test:integration-advanced && npm run test:websocket-stress && npm run test:ssr && npm run test:ssr-hydrator",
113
113
  "test:compiler": "node test/compiler.test.js",
114
114
  "test:sourcemap": "node test/sourcemap.test.js",
115
115
  "test:css-parsing": "node test/css-parsing.test.js",
116
116
  "test:pulse": "node test/pulse.test.js",
117
117
  "test:dom": "node test/dom.test.js",
118
118
  "test:dom-element": "node test/dom-element.test.js",
119
+ "test:dom-list": "node test/dom-list.test.js",
120
+ "test:dom-conditional": "node test/dom-conditional.test.js",
121
+ "test:dom-lifecycle": "node test/dom-lifecycle.test.js",
122
+ "test:dom-selector": "node test/dom-selector.test.js",
119
123
  "test:dom-adapter": "node test/dom-adapter.test.js",
124
+ "test:dom-advanced": "node test/dom-advanced.test.js",
120
125
  "test:enhanced-mock-adapter": "node test/enhanced-mock-adapter.test.js",
121
126
  "test:router": "node test/router.test.js",
122
127
  "test:store": "node test/store.test.js",
@@ -130,9 +135,11 @@
130
135
  "test:cli-create": "node test/cli-create.test.js",
131
136
  "test:lru-cache": "node test/lru-cache.test.js",
132
137
  "test:utils": "node test/utils.test.js",
138
+ "test:utils-coverage": "node test/utils-coverage.test.js",
133
139
  "test:docs": "node test/docs.test.js",
134
140
  "test:docs-nav": "node test/docs-navigation.test.js",
135
141
  "test:async": "node test/async.test.js",
142
+ "test:async-coverage": "node test/async-coverage.test.js",
136
143
  "test:form": "node test/form.test.js",
137
144
  "test:http": "node test/http.test.js",
138
145
  "test:devtools": "node test/devtools.test.js",
@@ -140,10 +147,12 @@
140
147
  "test:a11y": "node test/a11y.test.js",
141
148
  "test:a11y-enhanced": "node test/a11y-enhanced.test.js",
142
149
  "test:logger": "node test/logger.test.js",
150
+ "test:logger-prod": "node test/logger-prod.test.js",
143
151
  "test:errors": "node test/errors.test.js",
144
152
  "test:security": "node test/security.test.js",
145
153
  "test:websocket": "node test/websocket.test.js",
146
154
  "test:graphql": "node test/graphql.test.js",
155
+ "test:graphql-coverage": "node test/graphql-coverage.test.js",
147
156
  "test:doctor": "node test/doctor.test.js",
148
157
  "test:scaffold": "node test/scaffold.test.js",
149
158
  "test:test-runner": "node test/test-runner.test.js",
@@ -156,6 +165,7 @@
156
165
  "test:integration-advanced": "node test/integration-advanced.test.js",
157
166
  "test:websocket-stress": "node test/websocket-stress.test.js",
158
167
  "test:ssr": "node test/ssr.test.js",
168
+ "test:ssr-hydrator": "node test/ssr-hydrator.test.js",
159
169
  "build:netlify": "node scripts/build-netlify.js",
160
170
  "version": "node scripts/sync-version.js",
161
171
  "docs": "node cli/index.js dev docs"
package/runtime/async.js CHANGED
@@ -8,6 +8,7 @@
8
8
 
9
9
  import { pulse, effect, batch, onCleanup } from './pulse.js';
10
10
  import { getSSRAsyncContext, registerAsync, getCachedAsync, hasCachedAsync } from './ssr-async.js';
11
+ import { onWindowFocus, onWindowOnline, onNetworkChange } from './utils.js';
11
12
 
12
13
  // ============================================================================
13
14
  // Versioned Async - Centralized Race Condition Handling
@@ -694,26 +695,18 @@ export function useResource(key, fetcher, options = {}) {
694
695
  }
695
696
 
696
697
  // Setup window focus listener
697
- if (refreshOnFocus && typeof window !== 'undefined') {
698
- const handleFocus = () => {
698
+ if (refreshOnFocus) {
699
+ onWindowFocus(() => {
699
700
  const cached = getCachedData();
700
701
  if (!cached || cached.isStale) {
701
702
  fetch();
702
703
  }
703
- };
704
-
705
- window.addEventListener('focus', handleFocus);
706
- onCleanup(() => window.removeEventListener('focus', handleFocus));
704
+ }, onCleanup);
707
705
  }
708
706
 
709
707
  // Setup online listener
710
- if (refreshOnReconnect && typeof window !== 'undefined') {
711
- const handleOnline = () => {
712
- fetch();
713
- };
714
-
715
- window.addEventListener('online', handleOnline);
716
- onCleanup(() => window.removeEventListener('online', handleOnline));
708
+ if (refreshOnReconnect) {
709
+ onWindowOnline(() => fetch(), onCleanup);
717
710
  }
718
711
 
719
712
  // Track current key for change detection
@@ -868,19 +861,14 @@ export function usePolling(asyncFn, options) {
868
861
  }
869
862
 
870
863
  // Online/offline handling
871
- if (pauseOnOffline && typeof window !== 'undefined') {
872
- const handleOffline = () => pause();
873
- const handleOnline = () => {
874
- resume();
875
- if (isPolling.get()) poll();
876
- };
877
-
878
- window.addEventListener('offline', handleOffline);
879
- window.addEventListener('online', handleOnline);
880
- onCleanup(() => {
881
- window.removeEventListener('offline', handleOffline);
882
- window.removeEventListener('online', handleOnline);
883
- });
864
+ if (pauseOnOffline) {
865
+ onNetworkChange({
866
+ onOffline: () => pause(),
867
+ onOnline: () => {
868
+ resume();
869
+ if (isPolling.get()) poll();
870
+ }
871
+ }, onCleanup);
884
872
  }
885
873
 
886
874
  // Cleanup on unmount
@@ -29,7 +29,9 @@ export const config = {
29
29
  logUpdates: false,
30
30
  logEffects: false,
31
31
  warnOnSlowEffects: true,
32
- slowEffectThreshold: 16 // ms (one frame at 60fps)
32
+ slowEffectThreshold: 16, // ms (one frame at 60fps)
33
+ autoTimeline: false, // Automatically record all pulse changes to timeline
34
+ timelineDebounce: 100 // Debounce interval for timeline recording (ms)
33
35
  };
34
36
 
35
37
  // =============================================================================
@@ -51,6 +53,46 @@ export const effectRegistry = new Map();
51
53
  let pulseIdCounter = 0;
52
54
  let trackedEffectIdCounter = 0;
53
55
 
56
+ // Timeline auto-recording state
57
+ let timelineDebounceTimer = null;
58
+ let pendingTimelineActions = [];
59
+ let takeSnapshotFn = null;
60
+
61
+ /**
62
+ * Set the snapshot function for auto-timeline
63
+ * Called by devtools.js to wire up time-travel
64
+ * @param {Function} fn - takeSnapshot function
65
+ */
66
+ export function _setTakeSnapshotFn(fn) {
67
+ takeSnapshotFn = fn;
68
+ }
69
+
70
+ /**
71
+ * Schedule a timeline snapshot (debounced)
72
+ * @param {string} action - Description of the action
73
+ */
74
+ function scheduleTimelineSnapshot(action) {
75
+ if (!config.autoTimeline || !takeSnapshotFn) return;
76
+
77
+ pendingTimelineActions.push(action);
78
+
79
+ if (timelineDebounceTimer) {
80
+ clearTimeout(timelineDebounceTimer);
81
+ }
82
+
83
+ timelineDebounceTimer = setTimeout(() => {
84
+ timelineDebounceTimer = null;
85
+ if (pendingTimelineActions.length > 0) {
86
+ // Combine multiple actions into one snapshot
87
+ const combinedAction = pendingTimelineActions.length === 1
88
+ ? pendingTimelineActions[0]
89
+ : `${pendingTimelineActions.length} changes: ${pendingTimelineActions.slice(0, 3).join(', ')}${pendingTimelineActions.length > 3 ? '...' : ''}`;
90
+ pendingTimelineActions = [];
91
+ takeSnapshotFn(combinedAction);
92
+ }
93
+ }, config.timelineDebounce);
94
+ }
95
+
54
96
  // =============================================================================
55
97
  // DIAGNOSTICS API
56
98
  // =============================================================================
@@ -267,9 +309,14 @@ export function trackedPulse(initialValue, name, options = {}) {
267
309
  if (config.enabled && config.logUpdates) {
268
310
  log.info(`${name || id} updated:`, value);
269
311
  }
270
- if (config.enabled && options.onSnapshot) {
312
+ // Manual snapshot callback (for non-auto mode)
313
+ if (config.enabled && options.onSnapshot && !config.autoTimeline) {
271
314
  options.onSnapshot(`${name || id} = ${JSON.stringify(value)}`);
272
315
  }
316
+ // Auto-timeline recording (debounced)
317
+ if (config.enabled && config.autoTimeline) {
318
+ scheduleTimelineSnapshot(`${name || id} = ${JSON.stringify(value)}`);
319
+ }
273
320
  return result;
274
321
  };
275
322
 
@@ -399,5 +446,6 @@ export default {
399
446
  mark,
400
447
  resetDiagnostics,
401
448
  pulseRegistry,
402
- effectRegistry
449
+ effectRegistry,
450
+ _setTakeSnapshotFn
403
451
  };
@@ -31,7 +31,8 @@ import {
31
31
  profile,
32
32
  mark,
33
33
  resetDiagnostics,
34
- _setSnapshotCountFn
34
+ _setSnapshotCountFn,
35
+ _setTakeSnapshotFn
35
36
  } from './devtools/diagnostics.js';
36
37
 
37
38
  import {
@@ -50,6 +51,9 @@ import {
50
51
  // Wire up snapshot count for diagnostics
51
52
  _setSnapshotCountFn(getSnapshotCount);
52
53
 
54
+ // Wire up takeSnapshot for auto-timeline
55
+ _setTakeSnapshotFn(takeSnapshot);
56
+
53
57
  import {
54
58
  a11yAuditConfig,
55
59
  runA11yAudit,
@@ -88,9 +92,41 @@ export function trackedPulse(initialValue, name) {
88
92
  // DEV TOOLS API
89
93
  // =============================================================================
90
94
 
95
+ /**
96
+ * Enable automatic timeline recording
97
+ * Records all pulse changes to the timeline automatically (debounced)
98
+ * @param {Object} [options] - Configuration options
99
+ * @param {number} [options.debounce=100] - Debounce interval in ms
100
+ */
101
+ export function enableAutoTimeline(options = {}) {
102
+ config.autoTimeline = true;
103
+ if (options.debounce !== undefined) {
104
+ config.timelineDebounce = options.debounce;
105
+ }
106
+ log.info(`Auto-timeline enabled (debounce: ${config.timelineDebounce}ms)`);
107
+ }
108
+
109
+ /**
110
+ * Disable automatic timeline recording
111
+ */
112
+ export function disableAutoTimeline() {
113
+ config.autoTimeline = false;
114
+ log.info('Auto-timeline disabled');
115
+ }
116
+
117
+ /**
118
+ * Check if auto-timeline is enabled
119
+ * @returns {boolean}
120
+ */
121
+ export function isAutoTimelineEnabled() {
122
+ return config.autoTimeline;
123
+ }
124
+
91
125
  /**
92
126
  * Enable dev tools
93
127
  * @param {Object} [options] - Configuration options
128
+ * @param {boolean} [options.autoTimeline] - Enable automatic timeline recording
129
+ * @param {number} [options.timelineDebounce] - Debounce interval for timeline (ms)
94
130
  */
95
131
  export function enableDevTools(options = {}) {
96
132
  Object.assign(config, options, { enabled: true });
@@ -99,6 +135,14 @@ export function enableDevTools(options = {}) {
99
135
  timeTravelConfig.maxSnapshots = options.maxSnapshots;
100
136
  }
101
137
 
138
+ // Enable auto-timeline if specified
139
+ if (options.autoTimeline) {
140
+ config.autoTimeline = true;
141
+ if (options.timelineDebounce !== undefined) {
142
+ config.timelineDebounce = options.timelineDebounce;
143
+ }
144
+ }
145
+
102
146
  if (typeof window !== 'undefined') {
103
147
  // Expose to window for browser dev tools
104
148
  window.__PULSE_DEVTOOLS__ = {
@@ -119,6 +163,11 @@ export function enableDevTools(options = {}) {
119
163
  forward,
120
164
  clearHistory,
121
165
 
166
+ // Auto-timeline
167
+ enableAutoTimeline,
168
+ disableAutoTimeline,
169
+ isAutoTimelineEnabled,
170
+
122
171
  // A11y Audit
123
172
  runA11yAudit,
124
173
  getA11yIssues,
@@ -216,6 +265,9 @@ export {
216
265
  resetA11yAudit
217
266
  };
218
267
 
268
+ // Note: enableAutoTimeline, disableAutoTimeline, isAutoTimelineEnabled
269
+ // are already exported via their function declarations above
270
+
219
271
  // =============================================================================
220
272
  // DEFAULT EXPORT
221
273
  // =============================================================================
@@ -241,6 +293,11 @@ export default {
241
293
  forward,
242
294
  clearHistory,
243
295
 
296
+ // Auto-timeline
297
+ enableAutoTimeline,
298
+ disableAutoTimeline,
299
+ isAutoTimelineEnabled,
300
+
244
301
  // Configuration
245
302
  enableDevTools,
246
303
  disableDevTools,
package/runtime/errors.js CHANGED
@@ -217,6 +217,163 @@ export class RouterError extends RuntimeError {
217
217
  }
218
218
  }
219
219
 
220
+ // ============================================================================
221
+ // Client Errors (HTTP, WebSocket, GraphQL)
222
+ // ============================================================================
223
+
224
+ /**
225
+ * Base class for client errors (HTTP, WebSocket, GraphQL).
226
+ * Provides common patterns for error creation with suggestions.
227
+ *
228
+ * @example
229
+ * class HttpError extends ClientError {
230
+ * static suggestions = { TIMEOUT: 'Increase timeout...' };
231
+ * static errorName = 'HttpError';
232
+ * static defaultCode = 'HTTP_ERROR';
233
+ * static markerProperty = 'isHttpError';
234
+ * }
235
+ */
236
+ export class ClientError extends RuntimeError {
237
+ /** @type {Object<string, string>} Error code to suggestion mapping */
238
+ static suggestions = {};
239
+
240
+ /** @type {string} The error class name */
241
+ static errorName = 'ClientError';
242
+
243
+ /** @type {string} Default error code when none provided */
244
+ static defaultCode = 'CLIENT_ERROR';
245
+
246
+ /** @type {string} Property name for instance type checking (e.g., 'isHttpError') */
247
+ static markerProperty = 'isClientError';
248
+
249
+ /**
250
+ * @param {string} message - Error message
251
+ * @param {Object} [options={}] - Error options
252
+ * @param {string} [options.code] - Error code
253
+ * @param {string} [options.context] - Error context
254
+ * @param {string} [options.suggestion] - Custom suggestion (overrides default)
255
+ */
256
+ constructor(message, options = {}) {
257
+ const code = options.code || new.target.defaultCode;
258
+ const suggestion = options.suggestion || new.target.suggestions[code];
259
+
260
+ const formattedMessage = createErrorMessage({
261
+ code,
262
+ message,
263
+ context: options.context,
264
+ suggestion
265
+ });
266
+
267
+ super(formattedMessage, { code });
268
+
269
+ this.name = new.target.errorName;
270
+ this.code = code;
271
+ this[new.target.markerProperty] = true;
272
+ }
273
+
274
+ /**
275
+ * Check if an error is an instance of this error type
276
+ * @param {any} error - The error to check
277
+ * @returns {boolean}
278
+ */
279
+ static isError(error) {
280
+ return error?.[this.markerProperty] === true;
281
+ }
282
+
283
+ /**
284
+ * Check if this is a timeout error
285
+ * @returns {boolean}
286
+ */
287
+ isTimeout() {
288
+ return this.code === 'TIMEOUT';
289
+ }
290
+
291
+ /**
292
+ * Check if this is a network error
293
+ * @returns {boolean}
294
+ */
295
+ isNetworkError() {
296
+ return this.code === 'NETWORK' || this.code === 'NETWORK_ERROR';
297
+ }
298
+ }
299
+
300
+ /**
301
+ * Factory to create a client error class with specific suggestions and properties.
302
+ *
303
+ * @param {Object} config - Error class configuration
304
+ * @param {string} config.name - Error class name (e.g., 'HttpError')
305
+ * @param {string} config.defaultCode - Default error code
306
+ * @param {string} config.markerProperty - Instance marker property (e.g., 'isHttpError')
307
+ * @param {Object<string, string>} config.suggestions - Error code to suggestion mapping
308
+ * @param {string[]} [config.codeMethods] - Error codes to create isXxx() methods for
309
+ * @param {string[]} [config.additionalProperties] - Extra properties to copy from options
310
+ * @returns {typeof ClientError} The created error class
311
+ *
312
+ * @example
313
+ * const HttpError = createClientErrorClass({
314
+ * name: 'HttpError',
315
+ * defaultCode: 'HTTP_ERROR',
316
+ * markerProperty: 'isHttpError',
317
+ * suggestions: {
318
+ * TIMEOUT: 'Consider increasing the timeout.',
319
+ * NETWORK: 'Check internet connectivity.'
320
+ * },
321
+ * codeMethods: ['TIMEOUT', 'NETWORK', 'ABORT'],
322
+ * additionalProperties: ['config', 'request', 'response', 'status']
323
+ * });
324
+ */
325
+ export function createClientErrorClass(config) {
326
+ const {
327
+ name,
328
+ defaultCode,
329
+ markerProperty,
330
+ suggestions = {},
331
+ codeMethods = [],
332
+ additionalProperties = []
333
+ } = config;
334
+
335
+ class CustomClientError extends ClientError {
336
+ static suggestions = suggestions;
337
+ static errorName = name;
338
+ static defaultCode = defaultCode;
339
+ static markerProperty = markerProperty;
340
+
341
+ constructor(message, options = {}) {
342
+ super(message, options);
343
+
344
+ // Copy additional properties from options
345
+ for (const prop of additionalProperties) {
346
+ this[prop] = options[prop] ?? null;
347
+ }
348
+ }
349
+
350
+ /**
351
+ * Static type check method (e.g., HttpError.isHttpError(err))
352
+ */
353
+ static [`is${name}`](error) {
354
+ return error?.[markerProperty] === true;
355
+ }
356
+ }
357
+
358
+ // Add isXxx() methods for each code
359
+ for (const code of codeMethods) {
360
+ const methodName = 'is' + code.split('_').map(
361
+ (part, i) => i === 0
362
+ ? part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()
363
+ : part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()
364
+ ).join('');
365
+
366
+ CustomClientError.prototype[methodName] = function () {
367
+ return this.code === code;
368
+ };
369
+ }
370
+
371
+ // Set the class name for better debugging
372
+ Object.defineProperty(CustomClientError, 'name', { value: name });
373
+
374
+ return CustomClientError;
375
+ }
376
+
220
377
  // ============================================================================
221
378
  // CLI Errors
222
379
  // ============================================================================
@@ -564,6 +721,8 @@ export default {
564
721
  DOMError,
565
722
  StoreError,
566
723
  RouterError,
724
+ ClientError,
725
+ createClientErrorClass,
567
726
  CLIError,
568
727
  ConfigError,
569
728
  SUGGESTIONS,