pulse-js-framework 1.4.4 → 1.4.6

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/runtime/hmr.js ADDED
@@ -0,0 +1,169 @@
1
+ /**
2
+ * HMR (Hot Module Replacement) utilities for Pulse framework
3
+ * @module pulse-js-framework/runtime/hmr
4
+ *
5
+ * Provides state preservation and effect cleanup during hot module replacement.
6
+ *
7
+ * @example
8
+ * import { createHMRContext } from 'pulse-js-framework/runtime/hmr';
9
+ *
10
+ * const hmr = createHMRContext(import.meta.url);
11
+ *
12
+ * // State preserved across HMR updates
13
+ * const count = hmr.preservePulse('count', 0);
14
+ *
15
+ * // Effects tracked for cleanup
16
+ * hmr.setup(() => {
17
+ * effect(() => console.log(count.get()));
18
+ * });
19
+ */
20
+
21
+ import { pulse } from './pulse.js';
22
+ import { setCurrentModule, clearCurrentModule, disposeModule } from './pulse.js';
23
+
24
+ /**
25
+ * @typedef {Object} HMRContext
26
+ * @property {Object} data - Persistent data storage across HMR updates
27
+ * @property {function(string, *, Object=): Pulse} preservePulse - Create a pulse with preserved state
28
+ * @property {function(function): *} setup - Execute code with module tracking
29
+ * @property {function(function): void} accept - Register HMR accept callback
30
+ * @property {function(function): void} dispose - Register HMR dispose callback
31
+ */
32
+
33
+ /**
34
+ * Create an HMR context for a module.
35
+ * Provides utilities for state preservation and effect cleanup during HMR.
36
+ *
37
+ * @param {string} moduleId - The module identifier (typically import.meta.url)
38
+ * @returns {HMRContext} HMR context with preservation utilities
39
+ *
40
+ * @example
41
+ * const hmr = createHMRContext(import.meta.url);
42
+ *
43
+ * // Preserve state across HMR
44
+ * const todos = hmr.preservePulse('todos', []);
45
+ * const filter = hmr.preservePulse('filter', 'all');
46
+ *
47
+ * // Setup effects with automatic cleanup
48
+ * hmr.setup(() => {
49
+ * effect(() => {
50
+ * document.title = `${todos.get().length} todos`;
51
+ * });
52
+ * });
53
+ *
54
+ * // Accept HMR updates
55
+ * hmr.accept();
56
+ */
57
+ export function createHMRContext(moduleId) {
58
+ // Check if HMR is available (Vite dev server)
59
+ if (typeof import.meta === 'undefined' || !import.meta.hot) {
60
+ return createNoopContext();
61
+ }
62
+
63
+ const hot = import.meta.hot;
64
+
65
+ // Initialize data storage if not present
66
+ if (!hot.data) {
67
+ hot.data = {};
68
+ }
69
+
70
+ return {
71
+ /**
72
+ * Persistent data storage across HMR updates.
73
+ * Values stored here survive module reloads.
74
+ */
75
+ data: hot.data,
76
+
77
+ /**
78
+ * Create a pulse with state preservation across HMR updates.
79
+ * If a value exists from a previous module load, it's restored.
80
+ *
81
+ * @param {string} key - Unique key for this pulse within the module
82
+ * @param {*} initialValue - Initial value (used on first load only)
83
+ * @param {Object} [options] - Pulse options (equals function, etc.)
84
+ * @returns {Pulse} A pulse instance with preserved state
85
+ */
86
+ preservePulse(key, initialValue, options) {
87
+ const fullKey = `__pulse_${key}`;
88
+
89
+ // Check if we have a preserved value from previous load
90
+ if (fullKey in hot.data) {
91
+ const p = pulse(hot.data[fullKey], options);
92
+ // Register to save state on next HMR update
93
+ hot.dispose(() => {
94
+ hot.data[fullKey] = p.peek();
95
+ });
96
+ return p;
97
+ }
98
+
99
+ // First load - create new pulse with initial value
100
+ const p = pulse(initialValue, options);
101
+ // Register to save state on HMR update
102
+ hot.dispose(() => {
103
+ hot.data[fullKey] = p.peek();
104
+ });
105
+ return p;
106
+ },
107
+
108
+ /**
109
+ * Execute code with module tracking enabled.
110
+ * Effects created within this callback will be registered
111
+ * for automatic cleanup during HMR.
112
+ *
113
+ * @param {function} callback - Code to execute with tracking
114
+ * @returns {*} The return value of the callback
115
+ */
116
+ setup(callback) {
117
+ setCurrentModule(moduleId);
118
+ try {
119
+ return callback();
120
+ } finally {
121
+ clearCurrentModule();
122
+ }
123
+ },
124
+
125
+ /**
126
+ * Register a callback to run when the module accepts an HMR update.
127
+ *
128
+ * @param {function} [callback] - Optional callback for custom handling
129
+ */
130
+ accept(callback) {
131
+ if (callback) {
132
+ hot.accept(callback);
133
+ } else {
134
+ hot.accept();
135
+ }
136
+ },
137
+
138
+ /**
139
+ * Register a callback to run before the module is replaced.
140
+ * Use this for custom cleanup logic.
141
+ *
142
+ * @param {function} callback - Cleanup callback
143
+ */
144
+ dispose(callback) {
145
+ hot.dispose(callback);
146
+ }
147
+ };
148
+ }
149
+
150
+ /**
151
+ * Create a no-op HMR context for production or non-HMR environments.
152
+ * All methods work normally but without HMR-specific behavior.
153
+ *
154
+ * @returns {HMRContext} A no-op HMR context
155
+ * @private
156
+ */
157
+ function createNoopContext() {
158
+ return {
159
+ data: {},
160
+ preservePulse: (key, initialValue, options) => pulse(initialValue, options),
161
+ setup: (callback) => callback(),
162
+ accept: () => {},
163
+ dispose: () => {}
164
+ };
165
+ }
166
+
167
+ export default {
168
+ createHMRContext
169
+ };
package/runtime/index.js CHANGED
@@ -7,9 +7,11 @@ export * from './dom.js';
7
7
  export * from './router.js';
8
8
  export * from './store.js';
9
9
  export * from './native.js';
10
+ export * from './logger.js';
10
11
 
11
12
  export { default as PulseCore } from './pulse.js';
12
13
  export { default as PulseDOM } from './dom.js';
13
14
  export { default as PulseRouter } from './router.js';
14
15
  export { default as PulseStore } from './store.js';
15
16
  export { default as PulseNative } from './native.js';
17
+ export { default as PulseLogger } from './logger.js';
@@ -0,0 +1,304 @@
1
+ /**
2
+ * Pulse Logger - Centralized logging with namespaces and levels
3
+ * @module pulse-js-framework/runtime/logger
4
+ *
5
+ * @example
6
+ * import { logger, createLogger } from './logger.js';
7
+ *
8
+ * // Default logger
9
+ * logger.info('Hello');
10
+ * logger.warn('Warning');
11
+ * logger.error('Error');
12
+ *
13
+ * // Namespaced logger
14
+ * const log = createLogger('Router');
15
+ * log.info('Navigating to /home'); // [Router] Navigating to /home
16
+ */
17
+
18
+ /**
19
+ * Log level constants
20
+ * @readonly
21
+ * @enum {number}
22
+ */
23
+ export const LogLevel = {
24
+ /** No logging */
25
+ SILENT: 0,
26
+ /** Only errors */
27
+ ERROR: 1,
28
+ /** Errors and warnings */
29
+ WARN: 2,
30
+ /** Errors, warnings, and info (default) */
31
+ INFO: 3,
32
+ /** All messages including debug */
33
+ DEBUG: 4
34
+ };
35
+
36
+ /** @type {number} */
37
+ let globalLevel = LogLevel.INFO;
38
+
39
+ /** @type {LogFormatter|null} */
40
+ let globalFormatter = null;
41
+
42
+ /**
43
+ * @callback LogFormatter
44
+ * @param {'error'|'warn'|'info'|'debug'} level - The log level
45
+ * @param {string|null} namespace - The logger namespace
46
+ * @param {Array<*>} args - The arguments to log
47
+ * @returns {string} The formatted log message
48
+ */
49
+
50
+ /**
51
+ * Set the global log level for all loggers
52
+ * @param {number} level - A LogLevel value (SILENT=0, ERROR=1, WARN=2, INFO=3, DEBUG=4)
53
+ * @returns {void}
54
+ * @example
55
+ * setLogLevel(LogLevel.DEBUG); // Enable all logging
56
+ * setLogLevel(LogLevel.SILENT); // Disable all logging
57
+ */
58
+ export function setLogLevel(level) {
59
+ globalLevel = level;
60
+ }
61
+
62
+ /**
63
+ * Get the current global log level
64
+ * @returns {number} The current LogLevel value
65
+ * @example
66
+ * const level = getLogLevel();
67
+ * if (level >= LogLevel.DEBUG) {
68
+ * // Debug logging is enabled
69
+ * }
70
+ */
71
+ export function getLogLevel() {
72
+ return globalLevel;
73
+ }
74
+
75
+ /**
76
+ * Set a custom formatter function for all loggers
77
+ * @param {LogFormatter|null} formatter - Custom formatter function, or null to use default
78
+ * @returns {void}
79
+ * @example
80
+ * setFormatter((level, namespace, args) => {
81
+ * const timestamp = new Date().toISOString();
82
+ * const prefix = namespace ? `[${namespace}]` : '';
83
+ * return `${timestamp} ${level.toUpperCase()} ${prefix} ${args.join(' ')}`;
84
+ * });
85
+ */
86
+ export function setFormatter(formatter) {
87
+ globalFormatter = formatter;
88
+ }
89
+
90
+ /**
91
+ * Format message arguments with optional namespace prefix
92
+ * @private
93
+ * @param {string|null} namespace - The logger namespace
94
+ * @param {Array<*>} args - Arguments to format
95
+ * @returns {Array<*>} Formatted arguments array
96
+ */
97
+ function formatArgs(namespace, args) {
98
+ if (!namespace) return args;
99
+
100
+ // If first arg is a string, prepend namespace
101
+ if (typeof args[0] === 'string') {
102
+ return [`[${namespace}] ${args[0]}`, ...args.slice(1)];
103
+ }
104
+
105
+ // Otherwise, add namespace as first arg
106
+ return [`[${namespace}]`, ...args];
107
+ }
108
+
109
+ /**
110
+ * @typedef {Object} Logger
111
+ * @property {function(...*): void} error - Log error message
112
+ * @property {function(...*): void} warn - Log warning message
113
+ * @property {function(...*): void} info - Log info message
114
+ * @property {function(...*): void} debug - Log debug message
115
+ * @property {function(string): void} group - Start a collapsed log group
116
+ * @property {function(): void} groupEnd - End the current log group
117
+ * @property {function(number, ...*): void} log - Log with custom level
118
+ * @property {function(string): Logger} child - Create a child logger with sub-namespace
119
+ */
120
+
121
+ /**
122
+ * @typedef {Object} LoggerOptions
123
+ * @property {number} [level] - Override global level for this logger instance
124
+ */
125
+
126
+ /**
127
+ * Create a logger instance with optional namespace
128
+ * @param {string|null} [namespace=null] - Logger namespace (e.g., 'Router', 'Store')
129
+ * @param {LoggerOptions} [options={}] - Logger configuration options
130
+ * @returns {Logger} A logger instance with error, warn, info, debug methods
131
+ * @example
132
+ * const log = createLogger('MyComponent');
133
+ * log.info('Initialized'); // [MyComponent] Initialized
134
+ * log.error('Failed', { code: 500 }); // [MyComponent] Failed { code: 500 }
135
+ *
136
+ * // With custom level
137
+ * const debugLog = createLogger('Debug', { level: LogLevel.DEBUG });
138
+ */
139
+ export function createLogger(namespace = null, options = {}) {
140
+ const localLevel = options.level;
141
+
142
+ /**
143
+ * Check if a message at the given level should be logged
144
+ * @param {number} level - The log level to check
145
+ * @returns {boolean} True if the message should be logged
146
+ */
147
+ function shouldLog(level) {
148
+ const effectiveLevel = localLevel !== undefined ? localLevel : globalLevel;
149
+ return level <= effectiveLevel;
150
+ }
151
+
152
+ return {
153
+ /**
154
+ * Log an error message (shown unless level is SILENT)
155
+ * @param {...*} args - Values to log
156
+ * @returns {void}
157
+ */
158
+ error(...args) {
159
+ if (shouldLog(LogLevel.ERROR)) {
160
+ if (globalFormatter) {
161
+ console.error(globalFormatter('error', namespace, args));
162
+ } else {
163
+ console.error(...formatArgs(namespace, args));
164
+ }
165
+ }
166
+ },
167
+
168
+ /**
169
+ * Log a warning message (shown at WARN level and above)
170
+ * @param {...*} args - Values to log
171
+ * @returns {void}
172
+ */
173
+ warn(...args) {
174
+ if (shouldLog(LogLevel.WARN)) {
175
+ if (globalFormatter) {
176
+ console.warn(globalFormatter('warn', namespace, args));
177
+ } else {
178
+ console.warn(...formatArgs(namespace, args));
179
+ }
180
+ }
181
+ },
182
+
183
+ /**
184
+ * Log an info message (shown at INFO level and above)
185
+ * @param {...*} args - Values to log
186
+ * @returns {void}
187
+ */
188
+ info(...args) {
189
+ if (shouldLog(LogLevel.INFO)) {
190
+ if (globalFormatter) {
191
+ console.log(globalFormatter('info', namespace, args));
192
+ } else {
193
+ console.log(...formatArgs(namespace, args));
194
+ }
195
+ }
196
+ },
197
+
198
+ /**
199
+ * Log a debug message (only shown at DEBUG level)
200
+ * @param {...*} args - Values to log
201
+ * @returns {void}
202
+ */
203
+ debug(...args) {
204
+ if (shouldLog(LogLevel.DEBUG)) {
205
+ if (globalFormatter) {
206
+ console.log(globalFormatter('debug', namespace, args));
207
+ } else {
208
+ console.log(...formatArgs(namespace, args));
209
+ }
210
+ }
211
+ },
212
+
213
+ /**
214
+ * Start a collapsed log group (only shown at DEBUG level)
215
+ * @param {string} label - The group label
216
+ * @returns {void}
217
+ */
218
+ group(label) {
219
+ if (shouldLog(LogLevel.DEBUG)) {
220
+ console.group(namespace ? `[${namespace}] ${label}` : label);
221
+ }
222
+ },
223
+
224
+ /**
225
+ * End the current log group
226
+ * @returns {void}
227
+ */
228
+ groupEnd() {
229
+ if (shouldLog(LogLevel.DEBUG)) {
230
+ console.groupEnd();
231
+ }
232
+ },
233
+
234
+ /**
235
+ * Log a message at a custom level
236
+ * @param {number} level - The LogLevel to use
237
+ * @param {...*} args - Values to log
238
+ * @returns {void}
239
+ */
240
+ log(level, ...args) {
241
+ if (shouldLog(level)) {
242
+ const formatted = formatArgs(namespace, args);
243
+ switch (level) {
244
+ case LogLevel.ERROR:
245
+ console.error(...formatted);
246
+ break;
247
+ case LogLevel.WARN:
248
+ console.warn(...formatted);
249
+ break;
250
+ default:
251
+ console.log(...formatted);
252
+ }
253
+ }
254
+ },
255
+
256
+ /**
257
+ * Create a child logger with an additional namespace segment
258
+ * @param {string} childNamespace - The child namespace to append
259
+ * @returns {Logger} A new logger with combined namespace
260
+ * @example
261
+ * const log = createLogger('App');
262
+ * const routerLog = log.child('Router');
263
+ * routerLog.info('Navigate'); // [App:Router] Navigate
264
+ */
265
+ child(childNamespace) {
266
+ const combined = namespace
267
+ ? `${namespace}:${childNamespace}`
268
+ : childNamespace;
269
+ return createLogger(combined, options);
270
+ }
271
+ };
272
+ }
273
+
274
+ /**
275
+ * Default logger instance without namespace
276
+ * @type {Logger}
277
+ * @example
278
+ * import { logger } from './logger.js';
279
+ * logger.info('Application started');
280
+ */
281
+ export const logger = createLogger();
282
+
283
+ /**
284
+ * Pre-configured loggers for common Pulse subsystems
285
+ * @type {Object.<string, Logger>}
286
+ * @property {Logger} pulse - Logger for core reactivity system
287
+ * @property {Logger} dom - Logger for DOM operations
288
+ * @property {Logger} router - Logger for routing
289
+ * @property {Logger} store - Logger for state management
290
+ * @property {Logger} native - Logger for native/mobile features
291
+ * @property {Logger} hmr - Logger for hot module replacement
292
+ * @property {Logger} cli - Logger for CLI tools
293
+ */
294
+ export const loggers = {
295
+ pulse: createLogger('Pulse'),
296
+ dom: createLogger('DOM'),
297
+ router: createLogger('Router'),
298
+ store: createLogger('Store'),
299
+ native: createLogger('Native'),
300
+ hmr: createLogger('HMR'),
301
+ cli: createLogger('CLI')
302
+ };
303
+
304
+ export default logger;
package/runtime/native.js CHANGED
@@ -5,6 +5,9 @@
5
5
  */
6
6
 
7
7
  import { pulse, effect, batch } from './pulse.js';
8
+ import { loggers } from './logger.js';
9
+
10
+ const log = loggers.native;
8
11
 
9
12
  /**
10
13
  * Check if PulseMobile bridge is available
@@ -223,8 +226,8 @@ export const NativeUI = {
223
226
  if (isNativeAvailable()) {
224
227
  return getNative().UI.showToast(message, isLong);
225
228
  }
226
- // Fallback: simple console log
227
- console.log('[Toast]', message);
229
+ // Fallback: log toast message
230
+ log.info('Toast:', message);
228
231
  return Promise.resolve();
229
232
  },
230
233
 
@@ -335,7 +338,7 @@ export function exitApp() {
335
338
  if (isNativeAvailable() && getNative().isAndroid) {
336
339
  return getNative().App.exit();
337
340
  }
338
- console.warn('exitApp is only available on Android');
341
+ log.warn('exitApp is only available on Android');
339
342
  return Promise.resolve();
340
343
  }
341
344
 
@@ -346,7 +349,7 @@ export function minimizeApp() {
346
349
  if (isNativeAvailable()) {
347
350
  return getNative().App.minimize();
348
351
  }
349
- console.warn('minimizeApp is only available in native apps');
352
+ log.warn('minimizeApp is only available in native apps');
350
353
  return Promise.resolve();
351
354
  }
352
355