pulse-js-framework 1.7.4 → 1.7.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.
@@ -0,0 +1,575 @@
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.format();
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 → ${this.suggestion}\n`;
66
+ }
67
+
68
+ if (this.code && this.code !== 'PULSE_ERROR') {
69
+ output += `\n See: ${getDocsUrl(this.code)}\n`;
70
+ }
71
+
72
+ return output;
73
+ }
74
+
75
+ /**
76
+ * Format error without source code context
77
+ * @returns {string} Formatted error message
78
+ */
79
+ format() {
80
+ let output = `${this.name}: ${this.message}`;
81
+
82
+ if (this.file || this.line) {
83
+ output += `\n at ${this.file || '<anonymous>'}`;
84
+ if (this.line) output += `:${this.line}`;
85
+ if (this.column) output += `:${this.column}`;
86
+ }
87
+
88
+ if (this.suggestion) {
89
+ output += `\n\n → ${this.suggestion}`;
90
+ }
91
+
92
+ if (this.code && this.code !== 'PULSE_ERROR') {
93
+ output += `\n See: ${getDocsUrl(this.code)}`;
94
+ }
95
+
96
+ return output;
97
+ }
98
+
99
+ /**
100
+ * Convert to plain object for JSON serialization
101
+ * @returns {Object} Plain object representation
102
+ */
103
+ toJSON() {
104
+ return {
105
+ name: this.name,
106
+ message: this.message,
107
+ line: this.line,
108
+ column: this.column,
109
+ file: this.file,
110
+ code: this.code,
111
+ suggestion: this.suggestion
112
+ };
113
+ }
114
+ }
115
+
116
+ // ============================================================================
117
+ // Compiler Errors
118
+ // ============================================================================
119
+
120
+ /**
121
+ * Base class for all compilation errors
122
+ */
123
+ export class CompileError extends PulseError {
124
+ constructor(message, options = {}) {
125
+ super(message, { code: 'COMPILE_ERROR', ...options });
126
+ this.name = 'CompileError';
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Error during lexical analysis (tokenization)
132
+ */
133
+ export class LexerError extends CompileError {
134
+ constructor(message, options = {}) {
135
+ super(message, { code: 'LEXER_ERROR', ...options });
136
+ this.name = 'LexerError';
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Error during parsing (AST construction)
142
+ */
143
+ export class ParserError extends CompileError {
144
+ /**
145
+ * @param {string} message - Error message
146
+ * @param {Object} [options={}] - Error options
147
+ * @param {Object} [options.token] - The problematic token
148
+ */
149
+ constructor(message, options = {}) {
150
+ super(message, { code: 'PARSER_ERROR', ...options });
151
+ this.name = 'ParserError';
152
+ this.token = options.token ?? null;
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Error during code transformation/generation
158
+ */
159
+ export class TransformError extends CompileError {
160
+ constructor(message, options = {}) {
161
+ super(message, { code: 'TRANSFORM_ERROR', ...options });
162
+ this.name = 'TransformError';
163
+ }
164
+ }
165
+
166
+ // ============================================================================
167
+ // Runtime Errors
168
+ // ============================================================================
169
+
170
+ /**
171
+ * Base class for all runtime errors
172
+ */
173
+ export class RuntimeError extends PulseError {
174
+ constructor(message, options = {}) {
175
+ super(message, { code: 'RUNTIME_ERROR', ...options });
176
+ this.name = 'RuntimeError';
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Error in the reactivity system (effects, computed values)
182
+ */
183
+ export class ReactivityError extends RuntimeError {
184
+ constructor(message, options = {}) {
185
+ super(message, { code: 'REACTIVITY_ERROR', ...options });
186
+ this.name = 'ReactivityError';
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Error in DOM operations
192
+ */
193
+ export class DOMError extends RuntimeError {
194
+ constructor(message, options = {}) {
195
+ super(message, { code: 'DOM_ERROR', ...options });
196
+ this.name = 'DOMError';
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Error in store operations
202
+ */
203
+ export class StoreError extends RuntimeError {
204
+ constructor(message, options = {}) {
205
+ super(message, { code: 'STORE_ERROR', ...options });
206
+ this.name = 'StoreError';
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Error in router operations
212
+ */
213
+ export class RouterError extends RuntimeError {
214
+ constructor(message, options = {}) {
215
+ super(message, { code: 'ROUTER_ERROR', ...options });
216
+ this.name = 'RouterError';
217
+ }
218
+ }
219
+
220
+ // ============================================================================
221
+ // CLI Errors
222
+ // ============================================================================
223
+
224
+ /**
225
+ * Base class for CLI errors
226
+ */
227
+ export class CLIError extends PulseError {
228
+ constructor(message, options = {}) {
229
+ super(message, { code: 'CLI_ERROR', ...options });
230
+ this.name = 'CLIError';
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Configuration file error
236
+ */
237
+ export class ConfigError extends CLIError {
238
+ constructor(message, options = {}) {
239
+ super(message, { code: 'CONFIG_ERROR', ...options });
240
+ this.name = 'ConfigError';
241
+ }
242
+ }
243
+
244
+ // ============================================================================
245
+ // Documentation & URLs
246
+ // ============================================================================
247
+
248
+ const DOCS_BASE_URL = 'https://pulse-js.fr';
249
+
250
+ /**
251
+ * Generate documentation URL for an error code
252
+ * @param {string} code - Error code
253
+ * @returns {string} Documentation URL
254
+ */
255
+ export function getDocsUrl(code) {
256
+ const paths = {
257
+ // Reactivity
258
+ 'COMPUTED_SET': 'reactivity/computed#read-only',
259
+ 'CIRCULAR_DEPENDENCY': 'reactivity/effects#circular-dependencies',
260
+ 'EFFECT_ERROR': 'reactivity/effects#error-handling',
261
+ // DOM
262
+ 'MOUNT_ERROR': 'dom/mounting#troubleshooting',
263
+ 'SELECTOR_INVALID': 'dom/elements#selectors',
264
+ 'LIST_NO_KEY': 'dom/lists#key-function',
265
+ // Router
266
+ 'ROUTE_NOT_FOUND': 'router/routes#catch-all',
267
+ 'LAZY_TIMEOUT': 'router/lazy-loading#timeout',
268
+ 'GUARD_ERROR': 'router/guards#error-handling',
269
+ // Store
270
+ 'PERSIST_ERROR': 'store/persistence#troubleshooting',
271
+ 'STORE_TYPE_ERROR': 'store/state#valid-types',
272
+ // Compiler
273
+ 'PARSER_ERROR': 'compiler/syntax',
274
+ 'DUPLICATE_BLOCK': 'compiler/structure#blocks',
275
+ 'INVALID_DIRECTIVE': 'compiler/directives',
276
+ 'LEXER_ERROR': 'compiler/syntax#tokens'
277
+ };
278
+ return `${DOCS_BASE_URL}/${paths[code] || 'errors#' + code.toLowerCase()}`;
279
+ }
280
+
281
+ // ============================================================================
282
+ // Helper Functions
283
+ // ============================================================================
284
+
285
+ /**
286
+ * Common error suggestions based on error patterns
287
+ */
288
+ export const SUGGESTIONS = {
289
+ // Compiler
290
+ 'undefined-variable': (name) =>
291
+ `Did you forget to declare '${name}' in the state block or import it?`,
292
+ 'duplicate-declaration': (name) =>
293
+ `Remove the duplicate declaration of '${name}'`,
294
+ 'unexpected-token': (expected, got) =>
295
+ `Expected ${expected} but got '${got}'. Check for missing braces, quotes, or semicolons.`,
296
+ 'missing-closing-brace': () =>
297
+ `Add the missing closing brace '}'`,
298
+ 'missing-closing-paren': () =>
299
+ `Add the missing closing parenthesis ')'`,
300
+ 'invalid-directive': (name) =>
301
+ `'${name}' is not a valid directive. Valid directives: @if, @for, @click, @link, @outlet`,
302
+ 'empty-block': (blockName) =>
303
+ `The ${blockName} block is empty. Add content or remove the block.`,
304
+
305
+ // Reactivity
306
+ 'computed-set': (name) =>
307
+ `Use a regular pulse() if you need to set values directly, or modify the source pulse(s) that '${name || 'this computed'}' depends on.`,
308
+ 'circular-dependency': () =>
309
+ `Check for effects that modify their own dependencies. Consider using batch() to group updates.`,
310
+ 'effect-cleanup': () =>
311
+ `Return a cleanup function from your effect: effect(() => { ... return () => cleanup(); })`,
312
+
313
+ // DOM
314
+ 'mount-not-found': (selector) =>
315
+ `Ensure "${selector}" exists in the DOM. Use DOMContentLoaded or place script at end of <body>.`,
316
+ 'invalid-selector': (selector) =>
317
+ `"${selector}" is not a valid CSS selector. Use tag.class#id[attr] format.`,
318
+ 'list-needs-key': () =>
319
+ `Add a key function as third argument: list(items, render, item => item.id)`,
320
+
321
+ // Router
322
+ 'route-not-found': (path) =>
323
+ `Add a route for "${path}" or use a catch-all route: '/*path': NotFoundPage`,
324
+ 'lazy-timeout': (ms) =>
325
+ `The component took longer than ${ms}ms to load. Check your network or increase timeout.`,
326
+
327
+ // Store
328
+ 'persist-quota': () =>
329
+ `localStorage is full. Consider clearing old data or reducing state size.`,
330
+ 'invalid-store-value': (type) =>
331
+ `Store values must be serializable. ${type} cannot be persisted.`
332
+ };
333
+
334
+ /**
335
+ * Format an error with source code context
336
+ * @param {Error} error - The error to format
337
+ * @param {string} [source] - Source code for context
338
+ * @returns {string} Formatted error string
339
+ */
340
+ export function formatError(error, source) {
341
+ if (error instanceof PulseError) {
342
+ return error.formatWithSnippet(source);
343
+ }
344
+
345
+ // Handle plain Error objects with line/column properties
346
+ if (error.line && source) {
347
+ const pulseError = new PulseError(error.message, {
348
+ line: error.line,
349
+ column: error.column,
350
+ file: error.file
351
+ });
352
+ return pulseError.formatWithSnippet(source);
353
+ }
354
+
355
+ return error.stack || error.message;
356
+ }
357
+
358
+ /**
359
+ * Create a parser error from a token
360
+ * @param {string} message - Error message
361
+ * @param {Object} token - Token object with line/column info
362
+ * @param {Object} [options={}] - Additional options
363
+ * @returns {ParserError} The created error
364
+ */
365
+ export function createParserError(message, token, options = {}) {
366
+ return new ParserError(message, {
367
+ line: token?.line || 1,
368
+ column: token?.column || 1,
369
+ token,
370
+ ...options
371
+ });
372
+ }
373
+
374
+ /**
375
+ * Create a user-friendly error message with context and suggestions
376
+ * @param {Object} options - Error options
377
+ * @param {string} options.code - Error code (e.g., 'COMPUTED_SET')
378
+ * @param {string} options.message - Main error message
379
+ * @param {string} [options.context] - What the user was trying to do
380
+ * @param {string} [options.suggestion] - How to fix it
381
+ * @param {Object} [options.details] - Additional details
382
+ * @returns {string} Formatted error message
383
+ */
384
+ export function createErrorMessage({ code, message, context, suggestion, details }) {
385
+ let output = `[Pulse] ${message}`;
386
+
387
+ if (context) {
388
+ output += `\n\n Context: ${context}`;
389
+ }
390
+
391
+ if (details) {
392
+ for (const [key, value] of Object.entries(details)) {
393
+ output += `\n ${key}: ${value}`;
394
+ }
395
+ }
396
+
397
+ if (suggestion) {
398
+ output += `\n\n → ${suggestion}`;
399
+ }
400
+
401
+ if (code) {
402
+ output += `\n See: ${getDocsUrl(code)}`;
403
+ }
404
+
405
+ return output;
406
+ }
407
+
408
+ // ============================================================================
409
+ // Pre-built Error Creators
410
+ // ============================================================================
411
+
412
+ /**
413
+ * Error creators for common scenarios with good DX
414
+ */
415
+ export const Errors = {
416
+ /**
417
+ * Cannot set computed value
418
+ */
419
+ computedSet(name) {
420
+ return new ReactivityError(
421
+ createErrorMessage({
422
+ code: 'COMPUTED_SET',
423
+ message: `Cannot set computed value${name ? ` '${name}'` : ''}.`,
424
+ context: 'Computed values are derived from other pulses and update automatically.',
425
+ suggestion: SUGGESTIONS['computed-set'](name)
426
+ }),
427
+ { code: 'COMPUTED_SET' }
428
+ );
429
+ },
430
+
431
+ /**
432
+ * Circular dependency in effects
433
+ */
434
+ circularDependency(effectIds, pending) {
435
+ return new ReactivityError(
436
+ createErrorMessage({
437
+ code: 'CIRCULAR_DEPENDENCY',
438
+ message: 'Infinite loop detected in reactive effects.',
439
+ context: 'An effect is repeatedly triggering itself or other effects.',
440
+ details: {
441
+ 'Most active effects': effectIds.slice(0, 5).join(', '),
442
+ 'Still pending': pending.slice(0, 5).join(', ') || 'none'
443
+ },
444
+ suggestion: SUGGESTIONS['circular-dependency']()
445
+ }),
446
+ { code: 'CIRCULAR_DEPENDENCY' }
447
+ );
448
+ },
449
+
450
+ /**
451
+ * Mount target not found
452
+ */
453
+ mountNotFound(selector) {
454
+ return new DOMError(
455
+ createErrorMessage({
456
+ code: 'MOUNT_ERROR',
457
+ message: `Mount target not found: "${selector}"`,
458
+ context: 'The element must exist in the DOM before mounting.',
459
+ suggestion: SUGGESTIONS['mount-not-found'](selector)
460
+ }),
461
+ { code: 'MOUNT_ERROR' }
462
+ );
463
+ },
464
+
465
+ /**
466
+ * List missing key function
467
+ */
468
+ listNoKey() {
469
+ return new DOMError(
470
+ createErrorMessage({
471
+ code: 'LIST_NO_KEY',
472
+ message: 'List rendering without key function may cause performance issues.',
473
+ context: 'Keys help Pulse efficiently update only changed items.',
474
+ suggestion: SUGGESTIONS['list-needs-key']()
475
+ }),
476
+ { code: 'LIST_NO_KEY' }
477
+ );
478
+ },
479
+
480
+ /**
481
+ * Route not found
482
+ */
483
+ routeNotFound(path) {
484
+ return new RouterError(
485
+ createErrorMessage({
486
+ code: 'ROUTE_NOT_FOUND',
487
+ message: `No route matched: "${path}"`,
488
+ context: 'Navigation attempted to an undefined route.',
489
+ suggestion: SUGGESTIONS['route-not-found'](path)
490
+ }),
491
+ { code: 'ROUTE_NOT_FOUND' }
492
+ );
493
+ },
494
+
495
+ /**
496
+ * Lazy load timeout
497
+ */
498
+ lazyTimeout(timeout) {
499
+ return new RouterError(
500
+ createErrorMessage({
501
+ code: 'LAZY_TIMEOUT',
502
+ message: `Lazy component load timed out after ${timeout}ms.`,
503
+ context: 'The dynamic import took too long to resolve.',
504
+ suggestion: SUGGESTIONS['lazy-timeout'](timeout)
505
+ }),
506
+ { code: 'LAZY_TIMEOUT' }
507
+ );
508
+ },
509
+
510
+ /**
511
+ * Store persistence error
512
+ */
513
+ persistError(operation, cause) {
514
+ return new StoreError(
515
+ createErrorMessage({
516
+ code: 'PERSIST_ERROR',
517
+ message: `Failed to ${operation} persisted state.`,
518
+ context: cause?.message || 'localStorage operation failed.',
519
+ suggestion: SUGGESTIONS['persist-quota']()
520
+ }),
521
+ { code: 'PERSIST_ERROR' }
522
+ );
523
+ },
524
+
525
+ /**
526
+ * Invalid store value type
527
+ */
528
+ invalidStoreValue(type) {
529
+ return new StoreError(
530
+ createErrorMessage({
531
+ code: 'STORE_TYPE_ERROR',
532
+ message: `Invalid value type in store: ${type}`,
533
+ context: 'Store values must be JSON-serializable.',
534
+ suggestion: SUGGESTIONS['invalid-store-value'](type)
535
+ }),
536
+ { code: 'STORE_TYPE_ERROR' }
537
+ );
538
+ },
539
+
540
+ /**
541
+ * Native API not available
542
+ */
543
+ nativeNotAvailable(api) {
544
+ return new RuntimeError(
545
+ createErrorMessage({
546
+ code: 'NATIVE_ERROR',
547
+ message: `Native API '${api}' is not available.`,
548
+ context: 'This API only works in Pulse native mobile apps.',
549
+ suggestion: `Use isNativeAvailable() to check before calling native APIs, or use getPlatform() to detect the environment.`
550
+ }),
551
+ { code: 'NATIVE_ERROR' }
552
+ );
553
+ }
554
+ };
555
+
556
+ export default {
557
+ PulseError,
558
+ CompileError,
559
+ LexerError,
560
+ ParserError,
561
+ TransformError,
562
+ RuntimeError,
563
+ ReactivityError,
564
+ DOMError,
565
+ StoreError,
566
+ RouterError,
567
+ CLIError,
568
+ ConfigError,
569
+ SUGGESTIONS,
570
+ Errors,
571
+ formatError,
572
+ createParserError,
573
+ createErrorMessage,
574
+ getDocsUrl
575
+ };