pulse-js-framework 1.4.10 → 1.5.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.
package/core/errors.js ADDED
@@ -0,0 +1,300 @@
1
+ /**
2
+ * Pulse Error Classes - Structured error handling for the framework
3
+ * @module pulse-js-framework/core/errors
4
+ */
5
+
6
+ /**
7
+ * Base error class for all Pulse framework errors.
8
+ * Provides source location tracking and code snippet formatting.
9
+ */
10
+ export class PulseError extends Error {
11
+ /**
12
+ * @param {string} message - Error message
13
+ * @param {Object} [options={}] - Error options
14
+ * @param {number} [options.line] - Line number where error occurred
15
+ * @param {number} [options.column] - Column number where error occurred
16
+ * @param {string} [options.file] - Source file path
17
+ * @param {string} [options.code] - Error code for documentation lookup
18
+ * @param {string} [options.source] - Original source code
19
+ * @param {string} [options.suggestion] - Helpful suggestion to fix the error
20
+ */
21
+ constructor(message, options = {}) {
22
+ super(message);
23
+ this.name = 'PulseError';
24
+ this.line = options.line ?? null;
25
+ this.column = options.column ?? null;
26
+ this.file = options.file ?? null;
27
+ this.code = options.code ?? 'PULSE_ERROR';
28
+ this.source = options.source ?? null;
29
+ this.suggestion = options.suggestion ?? null;
30
+ }
31
+
32
+ /**
33
+ * Format the error with a code snippet showing context
34
+ * @param {string} [source] - Source code to use (overrides this.source)
35
+ * @param {number} [contextLines=2] - Number of lines to show before/after error
36
+ * @returns {string} Formatted error with code snippet
37
+ */
38
+ formatWithSnippet(source = this.source, contextLines = 2) {
39
+ if (!this.line || !source) {
40
+ return this.toString();
41
+ }
42
+
43
+ const lines = source.split('\n');
44
+ const start = Math.max(0, this.line - 1 - contextLines);
45
+ const end = Math.min(lines.length, this.line + contextLines);
46
+ const gutterWidth = String(end).length;
47
+
48
+ let output = `\n${this.name}: ${this.message}\n`;
49
+ output += ` at ${this.file || '<anonymous>'}:${this.line}:${this.column || 1}\n\n`;
50
+
51
+ for (let i = start; i < end; i++) {
52
+ const lineNum = i + 1;
53
+ const isErrorLine = lineNum === this.line;
54
+ const prefix = isErrorLine ? '>' : ' ';
55
+ const gutter = String(lineNum).padStart(gutterWidth);
56
+ output += `${prefix} ${gutter} | ${lines[i]}\n`;
57
+
58
+ if (isErrorLine && this.column) {
59
+ const padding = ' '.repeat(gutterWidth + 3 + Math.max(0, this.column - 1));
60
+ output += ` ${padding}^\n`;
61
+ }
62
+ }
63
+
64
+ if (this.suggestion) {
65
+ output += `\n Suggestion: ${this.suggestion}\n`;
66
+ }
67
+
68
+ if (this.code && this.code !== 'PULSE_ERROR') {
69
+ output += `\n Error code: ${this.code}\n`;
70
+ }
71
+
72
+ return output;
73
+ }
74
+
75
+ /**
76
+ * Convert to plain object for JSON serialization
77
+ * @returns {Object} Plain object representation
78
+ */
79
+ toJSON() {
80
+ return {
81
+ name: this.name,
82
+ message: this.message,
83
+ line: this.line,
84
+ column: this.column,
85
+ file: this.file,
86
+ code: this.code,
87
+ suggestion: this.suggestion
88
+ };
89
+ }
90
+ }
91
+
92
+ // ============================================================================
93
+ // Compiler Errors
94
+ // ============================================================================
95
+
96
+ /**
97
+ * Base class for all compilation errors
98
+ */
99
+ export class CompileError extends PulseError {
100
+ constructor(message, options = {}) {
101
+ super(message, { code: 'COMPILE_ERROR', ...options });
102
+ this.name = 'CompileError';
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Error during lexical analysis (tokenization)
108
+ */
109
+ export class LexerError extends CompileError {
110
+ constructor(message, options = {}) {
111
+ super(message, { code: 'LEXER_ERROR', ...options });
112
+ this.name = 'LexerError';
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Error during parsing (AST construction)
118
+ */
119
+ export class ParserError extends CompileError {
120
+ /**
121
+ * @param {string} message - Error message
122
+ * @param {Object} [options={}] - Error options
123
+ * @param {Object} [options.token] - The problematic token
124
+ */
125
+ constructor(message, options = {}) {
126
+ super(message, { code: 'PARSER_ERROR', ...options });
127
+ this.name = 'ParserError';
128
+ this.token = options.token ?? null;
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Error during code transformation/generation
134
+ */
135
+ export class TransformError extends CompileError {
136
+ constructor(message, options = {}) {
137
+ super(message, { code: 'TRANSFORM_ERROR', ...options });
138
+ this.name = 'TransformError';
139
+ }
140
+ }
141
+
142
+ // ============================================================================
143
+ // Runtime Errors
144
+ // ============================================================================
145
+
146
+ /**
147
+ * Base class for all runtime errors
148
+ */
149
+ export class RuntimeError extends PulseError {
150
+ constructor(message, options = {}) {
151
+ super(message, { code: 'RUNTIME_ERROR', ...options });
152
+ this.name = 'RuntimeError';
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Error in the reactivity system (effects, computed values)
158
+ */
159
+ export class ReactivityError extends RuntimeError {
160
+ constructor(message, options = {}) {
161
+ super(message, { code: 'REACTIVITY_ERROR', ...options });
162
+ this.name = 'ReactivityError';
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Error in DOM operations
168
+ */
169
+ export class DOMError extends RuntimeError {
170
+ constructor(message, options = {}) {
171
+ super(message, { code: 'DOM_ERROR', ...options });
172
+ this.name = 'DOMError';
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Error in store operations
178
+ */
179
+ export class StoreError extends RuntimeError {
180
+ constructor(message, options = {}) {
181
+ super(message, { code: 'STORE_ERROR', ...options });
182
+ this.name = 'StoreError';
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Error in router operations
188
+ */
189
+ export class RouterError extends RuntimeError {
190
+ constructor(message, options = {}) {
191
+ super(message, { code: 'ROUTER_ERROR', ...options });
192
+ this.name = 'RouterError';
193
+ }
194
+ }
195
+
196
+ // ============================================================================
197
+ // CLI Errors
198
+ // ============================================================================
199
+
200
+ /**
201
+ * Base class for CLI errors
202
+ */
203
+ export class CLIError extends PulseError {
204
+ constructor(message, options = {}) {
205
+ super(message, { code: 'CLI_ERROR', ...options });
206
+ this.name = 'CLIError';
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Configuration file error
212
+ */
213
+ export class ConfigError extends CLIError {
214
+ constructor(message, options = {}) {
215
+ super(message, { code: 'CONFIG_ERROR', ...options });
216
+ this.name = 'ConfigError';
217
+ }
218
+ }
219
+
220
+ // ============================================================================
221
+ // Helper Functions
222
+ // ============================================================================
223
+
224
+ /**
225
+ * Common error suggestions based on error patterns
226
+ */
227
+ export const SUGGESTIONS = {
228
+ 'undefined-variable': (name) =>
229
+ `Did you forget to declare '${name}' in the state block or import it?`,
230
+ 'duplicate-declaration': (name) =>
231
+ `Remove the duplicate declaration of '${name}'`,
232
+ 'unexpected-token': (expected, got) =>
233
+ `Expected ${expected} but got '${got}'. Check for missing braces, quotes, or semicolons.`,
234
+ 'missing-closing-brace': () =>
235
+ `Add the missing closing brace '}'`,
236
+ 'missing-closing-paren': () =>
237
+ `Add the missing closing parenthesis ')'`,
238
+ 'invalid-directive': (name) =>
239
+ `'${name}' is not a valid directive. Valid directives: @if, @each, @click, @link, @outlet`,
240
+ 'empty-block': (blockName) =>
241
+ `The ${blockName} block is empty. Add content or remove the block.`
242
+ };
243
+
244
+ /**
245
+ * Format an error with source code context
246
+ * @param {Error} error - The error to format
247
+ * @param {string} [source] - Source code for context
248
+ * @returns {string} Formatted error string
249
+ */
250
+ export function formatError(error, source) {
251
+ if (error instanceof PulseError) {
252
+ return error.formatWithSnippet(source);
253
+ }
254
+
255
+ // Handle plain Error objects with line/column properties
256
+ if (error.line && source) {
257
+ const pulseError = new PulseError(error.message, {
258
+ line: error.line,
259
+ column: error.column,
260
+ file: error.file
261
+ });
262
+ return pulseError.formatWithSnippet(source);
263
+ }
264
+
265
+ return error.stack || error.message;
266
+ }
267
+
268
+ /**
269
+ * Create a parser error from a token
270
+ * @param {string} message - Error message
271
+ * @param {Object} token - Token object with line/column info
272
+ * @param {Object} [options={}] - Additional options
273
+ * @returns {ParserError} The created error
274
+ */
275
+ export function createParserError(message, token, options = {}) {
276
+ return new ParserError(message, {
277
+ line: token?.line || 1,
278
+ column: token?.column || 1,
279
+ token,
280
+ ...options
281
+ });
282
+ }
283
+
284
+ export default {
285
+ PulseError,
286
+ CompileError,
287
+ LexerError,
288
+ ParserError,
289
+ TransformError,
290
+ RuntimeError,
291
+ ReactivityError,
292
+ DOMError,
293
+ StoreError,
294
+ RouterError,
295
+ CLIError,
296
+ ConfigError,
297
+ SUGGESTIONS,
298
+ formatError,
299
+ createParserError
300
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pulse-js-framework",
3
- "version": "1.4.10",
3
+ "version": "1.5.0",
4
4
  "description": "A declarative DOM framework with CSS selector-based structure and reactive pulsations",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -45,6 +45,8 @@
45
45
  "types": "./types/hmr.d.ts",
46
46
  "default": "./runtime/hmr.js"
47
47
  },
48
+ "./runtime/lru-cache": "./runtime/lru-cache.js",
49
+ "./runtime/utils": "./runtime/utils.js",
48
50
  "./compiler": {
49
51
  "types": "./types/index.d.ts",
50
52
  "default": "./compiler/index.js"
@@ -52,6 +54,7 @@
52
54
  "./compiler/lexer": "./compiler/lexer.js",
53
55
  "./compiler/parser": "./compiler/parser.js",
54
56
  "./compiler/transformer": "./compiler/transformer.js",
57
+ "./core/errors": "./core/errors.js",
55
58
  "./vite": {
56
59
  "types": "./types/index.d.ts",
57
60
  "default": "./loader/vite-plugin.js"
@@ -62,6 +65,7 @@
62
65
  "files": [
63
66
  "index.js",
64
67
  "cli/",
68
+ "core/",
65
69
  "runtime/",
66
70
  "compiler/",
67
71
  "loader/",
@@ -83,7 +87,8 @@
83
87
  "test:format": "node test/format.test.js",
84
88
  "test:analyze": "node test/analyze.test.js",
85
89
  "build:netlify": "node scripts/build-netlify.js",
86
- "version": "node scripts/sync-version.js"
90
+ "version": "node scripts/sync-version.js",
91
+ "docs": "node cli/index.js dev docs"
87
92
  },
88
93
  "keywords": [
89
94
  "framework",
package/runtime/dom.js CHANGED
@@ -7,12 +7,26 @@
7
7
 
8
8
  import { effect, pulse, batch, onCleanup } from './pulse.js';
9
9
  import { loggers } from './logger.js';
10
+ import { LRUCache } from './lru-cache.js';
10
11
 
11
12
  const log = loggers.dom;
12
13
 
13
- // Selector cache for parseSelector
14
- const selectorCache = new Map();
15
- const SELECTOR_CACHE_MAX = 500;
14
+ // =============================================================================
15
+ // SELECTOR CACHE
16
+ // =============================================================================
17
+ // LRU (Least Recently Used) cache for parseSelector results.
18
+ //
19
+ // Why LRU instead of Map?
20
+ // - Apps typically reuse the same selectors (e.g., 'div.container', 'button.primary')
21
+ // - Without eviction, memory grows unbounded in long-running apps
22
+ // - LRU keeps frequently-used selectors hot while evicting rare ones
23
+ //
24
+ // Capacity: 500 selectors (chosen based on typical SPA component count)
25
+ // - Most apps use 50-200 unique selectors
26
+ // - 500 provides headroom for dynamic selectors without excessive memory
27
+ //
28
+ // Cache hit returns a shallow copy to prevent mutation of cached config
29
+ const selectorCache = new LRUCache(500);
16
30
 
17
31
  // Lifecycle tracking
18
32
  let mountCallbacks = [];
@@ -113,12 +127,7 @@ export function parseSelector(selector) {
113
127
  config.attrs[key] = value;
114
128
  }
115
129
 
116
- // Cache the result (with size limit to prevent memory leaks)
117
- if (selectorCache.size >= SELECTOR_CACHE_MAX) {
118
- // Remove oldest entry (first key)
119
- const firstKey = selectorCache.keys().next().value;
120
- selectorCache.delete(firstKey);
121
- }
130
+ // Cache the result (LRU cache handles eviction automatically)
122
131
  selectorCache.set(selector, config);
123
132
 
124
133
  // Return a copy
@@ -311,6 +320,37 @@ export function on(element, event, handler, options) {
311
320
 
312
321
  /**
313
322
  * Create a reactive list with efficient keyed diffing
323
+ *
324
+ * LIST DIFFING ALGORITHM:
325
+ * -----------------------
326
+ * This uses a keyed reconciliation strategy to minimize DOM operations:
327
+ *
328
+ * 1. KEY EXTRACTION: Each item gets a unique key via keyFn (defaults to index)
329
+ * Good keys: item.id, item.uuid (stable across re-renders)
330
+ * Bad keys: array index (causes unnecessary re-renders on reorder)
331
+ *
332
+ * 2. RECONCILIATION PHASES:
333
+ * a) Build a map of new items by key
334
+ * b) For existing keys: reuse the DOM nodes (no re-creation)
335
+ * c) For removed keys: remove DOM nodes and run cleanup
336
+ * d) For new keys: create DOM nodes via template function
337
+ *
338
+ * 3. REORDERING: Uses a single forward pass:
339
+ * - Tracks previous node position with prevNode cursor
340
+ * - Only moves nodes that are out of position
341
+ * - Nodes already in correct position are skipped (no DOM operation)
342
+ *
343
+ * 4. BOUNDARY MARKERS: Uses comment nodes to track list boundaries:
344
+ * - startMarker: Insertion point for first item
345
+ * - endMarker: End boundary (not currently used but reserved)
346
+ *
347
+ * COMPLEXITY: O(n) for reconciliation + O(m) DOM moves where m <= n
348
+ * Best case (no changes): O(n) comparisons, 0 DOM operations
349
+ *
350
+ * @param {Function|Pulse} getItems - Items source (reactive)
351
+ * @param {Function} template - (item, index) => Node | Node[]
352
+ * @param {Function} keyFn - (item, index) => key (default: index)
353
+ * @returns {DocumentFragment} Container fragment with reactive list
314
354
  */
315
355
  export function list(getItems, template, keyFn = (item, i) => i) {
316
356
  const container = document.createDocumentFragment();
@@ -506,13 +546,24 @@ export function model(element, pulseValue) {
506
546
 
507
547
  /**
508
548
  * Mount an element to a target
549
+ * @param {string|HTMLElement} target - CSS selector or DOM element
550
+ * @param {Node} element - Element to mount
551
+ * @returns {Function} Unmount function
552
+ * @throws {Error} If target element is not found
509
553
  */
510
554
  export function mount(target, element) {
555
+ const originalTarget = target;
511
556
  if (typeof target === 'string') {
512
557
  target = document.querySelector(target);
513
558
  }
514
559
  if (!target) {
515
- throw new Error('Mount target not found');
560
+ const selector = typeof originalTarget === 'string' ? originalTarget : '(element)';
561
+ throw new Error(
562
+ `[Pulse] Mount target not found: "${selector}". ` +
563
+ `Ensure the element exists in the DOM before mounting. ` +
564
+ `Tip: Use document.addEventListener('DOMContentLoaded', () => mount(...)) ` +
565
+ `or place your script at the end of <body>.`
566
+ );
516
567
  }
517
568
  target.appendChild(element);
518
569
  return () => {
@@ -0,0 +1,145 @@
1
+ /**
2
+ * LRU (Least Recently Used) Cache
3
+ * @module pulse-js-framework/runtime/lru-cache
4
+ *
5
+ * A simple LRU cache implementation using Map's insertion order.
6
+ * When capacity is reached, the least recently used item is evicted.
7
+ */
8
+
9
+ /**
10
+ * LRU Cache implementation
11
+ * @template K, V
12
+ */
13
+ export class LRUCache {
14
+ /** @type {number} */
15
+ #capacity;
16
+
17
+ /** @type {Map<K, V>} */
18
+ #cache = new Map();
19
+
20
+ /**
21
+ * Create an LRU cache
22
+ * @param {number} capacity - Maximum number of items to store
23
+ */
24
+ constructor(capacity) {
25
+ if (capacity <= 0) {
26
+ throw new Error('LRU cache capacity must be greater than 0');
27
+ }
28
+ this.#capacity = capacity;
29
+ }
30
+
31
+ /**
32
+ * Get an item from the cache
33
+ * Accessing an item moves it to the "most recently used" position
34
+ * @param {K} key - Cache key
35
+ * @returns {V|undefined} The cached value or undefined if not found
36
+ */
37
+ get(key) {
38
+ if (!this.#cache.has(key)) {
39
+ return undefined;
40
+ }
41
+
42
+ // Move to end (most recently used) by re-inserting
43
+ const value = this.#cache.get(key);
44
+ this.#cache.delete(key);
45
+ this.#cache.set(key, value);
46
+ return value;
47
+ }
48
+
49
+ /**
50
+ * Set an item in the cache
51
+ * If the cache is at capacity, evicts the least recently used item
52
+ * @param {K} key - Cache key
53
+ * @param {V} value - Value to store
54
+ * @returns {LRUCache} this (for chaining)
55
+ */
56
+ set(key, value) {
57
+ // If key exists, delete first to update position
58
+ if (this.#cache.has(key)) {
59
+ this.#cache.delete(key);
60
+ } else if (this.#cache.size >= this.#capacity) {
61
+ // Remove oldest (first item in Map)
62
+ const oldest = this.#cache.keys().next().value;
63
+ this.#cache.delete(oldest);
64
+ }
65
+
66
+ this.#cache.set(key, value);
67
+ return this;
68
+ }
69
+
70
+ /**
71
+ * Check if a key exists in the cache
72
+ * Note: This does NOT update the item's position
73
+ * @param {K} key - Cache key
74
+ * @returns {boolean} True if key exists
75
+ */
76
+ has(key) {
77
+ return this.#cache.has(key);
78
+ }
79
+
80
+ /**
81
+ * Delete an item from the cache
82
+ * @param {K} key - Cache key
83
+ * @returns {boolean} True if item was deleted
84
+ */
85
+ delete(key) {
86
+ return this.#cache.delete(key);
87
+ }
88
+
89
+ /**
90
+ * Clear all items from the cache
91
+ */
92
+ clear() {
93
+ this.#cache.clear();
94
+ }
95
+
96
+ /**
97
+ * Get the current number of items in the cache
98
+ * @returns {number} Current size
99
+ */
100
+ get size() {
101
+ return this.#cache.size;
102
+ }
103
+
104
+ /**
105
+ * Get the maximum capacity of the cache
106
+ * @returns {number} Maximum capacity
107
+ */
108
+ get capacity() {
109
+ return this.#capacity;
110
+ }
111
+
112
+ /**
113
+ * Get all keys in the cache (oldest to newest)
114
+ * @returns {IterableIterator<K>} Iterator of keys
115
+ */
116
+ keys() {
117
+ return this.#cache.keys();
118
+ }
119
+
120
+ /**
121
+ * Get all values in the cache (oldest to newest)
122
+ * @returns {IterableIterator<V>} Iterator of values
123
+ */
124
+ values() {
125
+ return this.#cache.values();
126
+ }
127
+
128
+ /**
129
+ * Get all entries in the cache (oldest to newest)
130
+ * @returns {IterableIterator<[K, V]>} Iterator of [key, value] pairs
131
+ */
132
+ entries() {
133
+ return this.#cache.entries();
134
+ }
135
+
136
+ /**
137
+ * Iterate over all entries
138
+ * @param {function(V, K, LRUCache): void} callback - Called for each entry
139
+ */
140
+ forEach(callback) {
141
+ this.#cache.forEach((value, key) => callback(value, key, this));
142
+ }
143
+ }
144
+
145
+ export default LRUCache;
package/runtime/native.js CHANGED
@@ -21,7 +21,12 @@ export function isNativeAvailable() {
21
21
  */
22
22
  export function getNative() {
23
23
  if (!isNativeAvailable()) {
24
- throw new Error('PulseMobile is not available. Include pulse-native.js in your app.');
24
+ throw new Error(
25
+ '[Pulse Native] PulseMobile bridge is not available. ' +
26
+ 'This API only works in a Pulse native mobile app. ' +
27
+ 'For web, use isNativeAvailable() to check before calling native APIs, ' +
28
+ 'or use getPlatform() to detect the current environment.'
29
+ );
25
30
  }
26
31
  return window.PulseMobile;
27
32
  }
package/runtime/pulse.js CHANGED
@@ -22,6 +22,43 @@ import { loggers } from './logger.js';
22
22
 
23
23
  const log = loggers.pulse;
24
24
 
25
+ // =============================================================================
26
+ // REACTIVE DEPENDENCY TRACKING ALGORITHM
27
+ // =============================================================================
28
+ //
29
+ // This module implements a push-based reactive system using automatic dependency
30
+ // tracking. Here's how it works:
31
+ //
32
+ // 1. READING (Dependency Collection)
33
+ // - When an effect runs, `context.currentEffect` points to it
34
+ // - When a pulse's get() is called, it checks if there's a currentEffect
35
+ // - If so, it adds the effect to its subscribers and vice versa
36
+ // - This creates a bidirectional link: pulse knows who depends on it,
37
+ // effect knows what it depends on
38
+ //
39
+ // 2. WRITING (Change Notification)
40
+ // - When a pulse's set() is called with a new value, it notifies subscribers
41
+ // - Each subscriber (effect) is either run immediately or queued for batching
42
+ // - Effects re-run, clearing old dependencies and collecting new ones
43
+ //
44
+ // 3. BATCHING (Performance Optimization)
45
+ // - batch() increments batchDepth and delays effect execution
46
+ // - Effects are queued in pendingEffects instead of running immediately
47
+ // - When batch completes, flushEffects() runs all queued effects
48
+ // - Nested batches are handled by only flushing at depth 0
49
+ //
50
+ // 4. COMPUTED VALUES (Derived State)
51
+ // - Computed pulses wrap a function and cache its result
52
+ // - They use the same dependency tracking to know when to recompute
53
+ // - Lazy computed values only recompute when read (pull-based optimization)
54
+ //
55
+ // 5. CLEANUP (Memory Management)
56
+ // - Effects can return a cleanup function called before re-run or disposal
57
+ // - On re-run, old dependencies are cleared before collecting new ones
58
+ // - This prevents memory leaks and stale subscriptions
59
+ //
60
+ // =============================================================================
61
+
25
62
  /**
26
63
  * @typedef {Object} ReactiveContext
27
64
  * @property {EffectFn|null} currentEffect - Currently executing effect for dependency tracking
@@ -509,11 +546,18 @@ export function computed(fn, options = {}) {
509
546
 
510
547
  // Override set to make it read-only
511
548
  p.set = () => {
512
- throw new Error('Cannot set a computed pulse directly');
549
+ throw new Error(
550
+ '[Pulse] Cannot set a computed value directly. ' +
551
+ 'Computed values are derived from other pulses and update automatically. ' +
552
+ 'Modify the source pulse(s) instead.'
553
+ );
513
554
  };
514
555
 
515
556
  p.update = () => {
516
- throw new Error('Cannot update a computed pulse directly');
557
+ throw new Error(
558
+ '[Pulse] Cannot update a computed value directly. ' +
559
+ 'Computed values are read-only. Modify the source pulse(s) instead.'
560
+ );
517
561
  };
518
562
 
519
563
  // Add dispose method
package/runtime/router.js CHANGED
@@ -15,6 +15,9 @@
15
15
 
16
16
  import { pulse, effect, batch } from './pulse.js';
17
17
  import { el } from './dom.js';
18
+ import { loggers } from './logger.js';
19
+
20
+ const log = loggers.router;
18
21
 
19
22
  /**
20
23
  * Lazy load helper for route components
@@ -126,7 +129,7 @@ export function lazy(importFn, options = {}) {
126
129
  if (ErrorComponent) {
127
130
  container.replaceChildren(ErrorComponent(err));
128
131
  } else {
129
- console.error('Lazy load error:', err);
132
+ log.error('Lazy load error:', err);
130
133
  container.replaceChildren(
131
134
  el('div.lazy-error', `Failed to load component: ${err.message}`)
132
135
  );