mastercontroller 1.3.6 → 1.3.7

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,353 @@
1
+ /**
2
+ * ErrorBoundary - Production error boundary system for Web Components
3
+ * Catches component errors without crashing entire application
4
+ * Version: 1.0.0
5
+ */
6
+
7
+ /**
8
+ * ErrorBoundary Web Component
9
+ * Usage:
10
+ * <error-boundary>
11
+ * <my-component></my-component>
12
+ * </error-boundary>
13
+ */
14
+ class ErrorBoundary extends HTMLElement {
15
+ constructor() {
16
+ super();
17
+ this._hasError = false;
18
+ this._errorInfo = null;
19
+ this._originalContent = null;
20
+ }
21
+
22
+ connectedCallback() {
23
+ // Store original content
24
+ this._originalContent = this.innerHTML;
25
+
26
+ // Catch errors from child components
27
+ this.addEventListener('error', this._handleError.bind(this), true);
28
+
29
+ // Also catch unhandled promise rejections in child components
30
+ window.addEventListener('unhandledrejection', this._handleRejection.bind(this));
31
+
32
+ // Wrap all child custom elements with error catching
33
+ this._wrapChildComponents();
34
+ }
35
+
36
+ disconnectedCallback() {
37
+ this.removeEventListener('error', this._handleError, true);
38
+ window.removeEventListener('unhandledrejection', this._handleRejection);
39
+ }
40
+
41
+ /**
42
+ * Wrap child component lifecycle methods with error handling
43
+ */
44
+ _wrapChildComponents() {
45
+ const customElements = this.querySelectorAll('*');
46
+
47
+ customElements.forEach(el => {
48
+ if (!el.tagName.includes('-')) return;
49
+
50
+ // Wrap connectedCallback
51
+ if (el.connectedCallback && !el._errorBoundaryWrapped) {
52
+ const originalConnected = el.connectedCallback.bind(el);
53
+ el.connectedCallback = (...args) => {
54
+ try {
55
+ return originalConnected(...args);
56
+ } catch (error) {
57
+ this._catchComponentError(error, el);
58
+ }
59
+ };
60
+ el._errorBoundaryWrapped = true;
61
+ }
62
+
63
+ // Wrap attributeChangedCallback
64
+ if (el.attributeChangedCallback && !el._errorBoundaryAttrWrapped) {
65
+ const originalAttrChanged = el.attributeChangedCallback.bind(el);
66
+ el.attributeChangedCallback = (...args) => {
67
+ try {
68
+ return originalAttrChanged(...args);
69
+ } catch (error) {
70
+ this._catchComponentError(error, el);
71
+ }
72
+ };
73
+ el._errorBoundaryAttrWrapped = true;
74
+ }
75
+ });
76
+ }
77
+
78
+ /**
79
+ * Handle error events
80
+ */
81
+ _handleError(event) {
82
+ // Only handle errors from child elements
83
+ if (!this.contains(event.target)) return;
84
+
85
+ event.preventDefault();
86
+ event.stopPropagation();
87
+
88
+ this._catchComponentError(event.error || new Error('Unknown error'), event.target);
89
+ }
90
+
91
+ /**
92
+ * Handle unhandled promise rejections
93
+ */
94
+ _handleRejection(event) {
95
+ // Check if rejection came from a component within this boundary
96
+ if (event.reason && event.reason.component) {
97
+ const component = this.querySelector(event.reason.component);
98
+ if (component) {
99
+ this._catchComponentError(event.reason, component);
100
+ }
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Catch and handle component errors
106
+ */
107
+ _catchComponentError(error, component) {
108
+ if (this._hasError) return; // Already in error state
109
+
110
+ this._hasError = true;
111
+ this._errorInfo = {
112
+ error,
113
+ component: component ? component.tagName.toLowerCase() : 'unknown',
114
+ timestamp: new Date().toISOString(),
115
+ userAgent: navigator.userAgent,
116
+ url: window.location.href
117
+ };
118
+
119
+ // Log error
120
+ this._logError();
121
+
122
+ // Show fallback UI
123
+ this._showFallbackUI();
124
+
125
+ // Call custom error handler if provided
126
+ if (typeof this.onError === 'function') {
127
+ try {
128
+ this.onError(this._errorInfo);
129
+ } catch (handlerError) {
130
+ console.error('[ErrorBoundary] onError handler failed:', handlerError);
131
+ }
132
+ }
133
+
134
+ // Dispatch custom event for external monitoring
135
+ this.dispatchEvent(new CustomEvent('error-boundary-catch', {
136
+ bubbles: true,
137
+ detail: this._errorInfo
138
+ }));
139
+ }
140
+
141
+ /**
142
+ * Log error to console and monitoring services
143
+ */
144
+ _logError() {
145
+ console.error('[ErrorBoundary] Caught error:', this._errorInfo);
146
+
147
+ // Send to monitoring service if configured
148
+ if (window.masterControllerErrorReporter) {
149
+ try {
150
+ window.masterControllerErrorReporter({
151
+ type: 'error-boundary',
152
+ ...this._errorInfo
153
+ });
154
+ } catch (reporterError) {
155
+ console.error('[ErrorBoundary] Error reporter failed:', reporterError);
156
+ }
157
+ }
158
+
159
+ // Send to Sentry if available
160
+ if (window.Sentry) {
161
+ window.Sentry.captureException(this._errorInfo.error, {
162
+ tags: {
163
+ component: this._errorInfo.component,
164
+ errorBoundary: true
165
+ },
166
+ extra: this._errorInfo
167
+ });
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Show fallback UI
173
+ */
174
+ _showFallbackUI() {
175
+ const fallbackTemplate = this.getAttribute('fallback-template');
176
+ const customMessage = this.getAttribute('error-message');
177
+
178
+ if (fallbackTemplate) {
179
+ // Use custom template
180
+ const template = document.querySelector(fallbackTemplate);
181
+ if (template) {
182
+ this.innerHTML = template.innerHTML;
183
+ return;
184
+ }
185
+ }
186
+
187
+ // Default fallback UI
188
+ const isDevelopment = this.hasAttribute('dev-mode');
189
+
190
+ this.innerHTML = `
191
+ <div class="error-boundary-fallback" style="
192
+ padding: 20px;
193
+ margin: 10px 0;
194
+ background: ${isDevelopment ? '#fee' : '#f9fafb'};
195
+ border: 2px solid ${isDevelopment ? '#f87171' : '#d1d5db'};
196
+ border-radius: 8px;
197
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
198
+ ">
199
+ <div style="display: flex; align-items: start; gap: 12px;">
200
+ <div style="font-size: 24px;">${isDevelopment ? '❌' : '⚠️'}</div>
201
+ <div style="flex: 1;">
202
+ <h3 style="margin: 0 0 8px 0; color: ${isDevelopment ? '#dc2626' : '#374151'}; font-size: 18px; font-weight: 600;">
203
+ ${customMessage || 'Something went wrong'}
204
+ </h3>
205
+ <p style="margin: 0 0 12px 0; color: #6b7280; font-size: 14px;">
206
+ ${isDevelopment
207
+ ? `Component "${this._errorInfo.component}" encountered an error.`
208
+ : 'We\'ve been notified and are working on it.'
209
+ }
210
+ </p>
211
+ ${isDevelopment ? `
212
+ <details style="margin-top: 12px;">
213
+ <summary style="cursor: pointer; color: #3b82f6; font-weight: 600; font-size: 14px;">
214
+ View Error Details
215
+ </summary>
216
+ <pre style="
217
+ margin-top: 12px;
218
+ padding: 12px;
219
+ background: #1f2937;
220
+ color: #f3f4f6;
221
+ border-radius: 4px;
222
+ font-size: 12px;
223
+ overflow-x: auto;
224
+ font-family: 'Courier New', monospace;
225
+ ">${this.escapeHtml(this._errorInfo.error.stack || this._errorInfo.error.message)}</pre>
226
+ </details>
227
+ ` : ''}
228
+ <button
229
+ onclick="this.closest('.error-boundary-fallback').parentElement.dispatchEvent(new CustomEvent('error-boundary-retry', { bubbles: true }))"
230
+ style="
231
+ margin-top: 12px;
232
+ padding: 8px 16px;
233
+ background: #3b82f6;
234
+ color: white;
235
+ border: none;
236
+ border-radius: 6px;
237
+ font-weight: 600;
238
+ font-size: 14px;
239
+ cursor: pointer;
240
+ "
241
+ onmouseover="this.style.background='#2563eb'"
242
+ onmouseout="this.style.background='#3b82f6'"
243
+ >
244
+ Try Again
245
+ </button>
246
+ </div>
247
+ </div>
248
+ </div>
249
+ `;
250
+
251
+ // Handle retry button
252
+ this.addEventListener('error-boundary-retry', this._handleRetry.bind(this), { once: true });
253
+ }
254
+
255
+ /**
256
+ * Handle retry
257
+ */
258
+ _handleRetry() {
259
+ this._hasError = false;
260
+ this._errorInfo = null;
261
+ this.innerHTML = this._originalContent;
262
+
263
+ // Re-wrap child components
264
+ this._wrapChildComponents();
265
+
266
+ // Dispatch retry event
267
+ this.dispatchEvent(new CustomEvent('error-boundary-retried', {
268
+ bubbles: true
269
+ }));
270
+ }
271
+
272
+ /**
273
+ * Escape HTML for safe rendering
274
+ */
275
+ escapeHtml(str) {
276
+ if (!str) return '';
277
+ return String(str)
278
+ .replace(/&/g, '&amp;')
279
+ .replace(/</g, '&lt;')
280
+ .replace(/>/g, '&gt;')
281
+ .replace(/"/g, '&quot;')
282
+ .replace(/'/g, '&#039;');
283
+ }
284
+
285
+ /**
286
+ * Public API: Reset error state
287
+ */
288
+ reset() {
289
+ this._handleRetry();
290
+ }
291
+
292
+ /**
293
+ * Public API: Get error info
294
+ */
295
+ getErrorInfo() {
296
+ return this._errorInfo;
297
+ }
298
+
299
+ /**
300
+ * Public API: Check if has error
301
+ */
302
+ hasError() {
303
+ return this._hasError;
304
+ }
305
+ }
306
+
307
+ // Register the error boundary component
308
+ if (!customElements.get('error-boundary')) {
309
+ customElements.define('error-boundary', ErrorBoundary);
310
+ }
311
+
312
+ // Global error handler setup
313
+ if (typeof window !== 'undefined') {
314
+ // Catch uncaught errors globally
315
+ window.addEventListener('error', (event) => {
316
+ console.error('[MasterController] Uncaught error:', event.error);
317
+
318
+ // Try to find nearest error boundary
319
+ if (event.target instanceof HTMLElement) {
320
+ let boundary = event.target.closest('error-boundary');
321
+ if (boundary) {
322
+ // Error will be handled by the boundary
323
+ return;
324
+ }
325
+ }
326
+
327
+ // No boundary found - log to monitoring service
328
+ if (window.masterControllerErrorReporter) {
329
+ window.masterControllerErrorReporter({
330
+ type: 'uncaught-error',
331
+ error: event.error,
332
+ message: event.message,
333
+ filename: event.filename,
334
+ lineno: event.lineno,
335
+ colno: event.colno
336
+ });
337
+ }
338
+ });
339
+
340
+ // Catch unhandled promise rejections
341
+ window.addEventListener('unhandledrejection', (event) => {
342
+ console.error('[MasterController] Unhandled rejection:', event.reason);
343
+
344
+ if (window.masterControllerErrorReporter) {
345
+ window.masterControllerErrorReporter({
346
+ type: 'unhandled-rejection',
347
+ reason: event.reason
348
+ });
349
+ }
350
+ });
351
+ }
352
+
353
+ export { ErrorBoundary };
@@ -0,0 +1,265 @@
1
+ /**
2
+ * HydrationMismatch - Detect and report hydration mismatches
3
+ * Compares server-rendered HTML with client-rendered HTML
4
+ * Version: 1.0.0
5
+ */
6
+
7
+ const isDevelopment = typeof process !== 'undefined'
8
+ ? (process.env.NODE_ENV !== 'production')
9
+ : (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1');
10
+
11
+ /**
12
+ * Simple diff algorithm for HTML comparison
13
+ */
14
+ function generateDiff(serverHTML, clientHTML) {
15
+ const serverLines = serverHTML.split('\n').map(l => l.trim()).filter(Boolean);
16
+ const clientLines = clientHTML.split('\n').map(l => l.trim()).filter(Boolean);
17
+
18
+ const diff = [];
19
+ const maxLines = Math.max(serverLines.length, clientLines.length);
20
+
21
+ for (let i = 0; i < maxLines; i++) {
22
+ const serverLine = serverLines[i] || '';
23
+ const clientLine = clientLines[i] || '';
24
+
25
+ if (serverLine !== clientLine) {
26
+ diff.push({
27
+ line: i + 1,
28
+ server: serverLine,
29
+ client: clientLine,
30
+ type: !serverLine ? 'added' : !clientLine ? 'removed' : 'modified'
31
+ });
32
+ }
33
+ }
34
+
35
+ return diff;
36
+ }
37
+
38
+ /**
39
+ * Format diff for console output
40
+ */
41
+ function formatDiffForConsole(diff) {
42
+ let output = '\n';
43
+
44
+ diff.slice(0, 10).forEach(change => { // Show first 10 differences
45
+ output += `Line ${change.line}:\n`;
46
+
47
+ if (change.type === 'removed') {
48
+ output += ` \x1b[31m- ${change.server}\x1b[0m\n`;
49
+ } else if (change.type === 'added') {
50
+ output += ` \x1b[32m+ ${change.client}\x1b[0m\n`;
51
+ } else {
52
+ output += ` \x1b[31m- ${change.server}\x1b[0m\n`;
53
+ output += ` \x1b[32m+ ${change.client}\x1b[0m\n`;
54
+ }
55
+ });
56
+
57
+ if (diff.length > 10) {
58
+ output += `\n... and ${diff.length - 10} more differences\n`;
59
+ }
60
+
61
+ return output;
62
+ }
63
+
64
+ /**
65
+ * Compare attributes between two elements
66
+ */
67
+ function compareAttributes(serverEl, clientEl) {
68
+ const mismatches = [];
69
+
70
+ // Check server attributes
71
+ if (serverEl.attributes) {
72
+ for (const attr of serverEl.attributes) {
73
+ const serverValue = attr.value;
74
+ const clientValue = clientEl.getAttribute(attr.name);
75
+
76
+ if (serverValue !== clientValue) {
77
+ mismatches.push({
78
+ attribute: attr.name,
79
+ server: serverValue,
80
+ client: clientValue || '(missing)'
81
+ });
82
+ }
83
+ }
84
+ }
85
+
86
+ // Check for client attributes missing on server
87
+ if (clientEl.attributes) {
88
+ for (const attr of clientEl.attributes) {
89
+ if (!serverEl.hasAttribute(attr.name)) {
90
+ mismatches.push({
91
+ attribute: attr.name,
92
+ server: '(missing)',
93
+ client: attr.value
94
+ });
95
+ }
96
+ }
97
+ }
98
+
99
+ return mismatches;
100
+ }
101
+
102
+ /**
103
+ * Detect hydration mismatch between server and client HTML
104
+ */
105
+ function detectHydrationMismatch(element, componentName, options = {}) {
106
+ if (!element || !element.hasAttribute('data-ssr')) {
107
+ return null; // Not server-rendered
108
+ }
109
+
110
+ // Store server HTML before hydration
111
+ const serverHTML = element.innerHTML;
112
+
113
+ // Create a clone to test client rendering
114
+ const testElement = element.cloneNode(false);
115
+ testElement.removeAttribute('data-ssr');
116
+
117
+ // Simulate client render
118
+ if (typeof element.connectedCallback === 'function') {
119
+ try {
120
+ // Call connectedCallback to trigger client render
121
+ const originalCallback = element.constructor.prototype.connectedCallback;
122
+ if (originalCallback) {
123
+ originalCallback.call(testElement);
124
+ }
125
+ } catch (error) {
126
+ console.warn('[HydrationMismatch] Could not simulate client render:', error);
127
+ return null;
128
+ }
129
+ }
130
+
131
+ const clientHTML = testElement.innerHTML;
132
+
133
+ // Compare HTML
134
+ if (serverHTML.trim() === clientHTML.trim()) {
135
+ return null; // No mismatch
136
+ }
137
+
138
+ // Generate diff
139
+ const diff = generateDiff(serverHTML, clientHTML);
140
+
141
+ // Compare attributes
142
+ const attrMismatches = compareAttributes(element, testElement);
143
+
144
+ return {
145
+ component: componentName || element.tagName.toLowerCase(),
146
+ serverHTML,
147
+ clientHTML,
148
+ diff,
149
+ attrMismatches,
150
+ element
151
+ };
152
+ }
153
+
154
+ /**
155
+ * Report hydration mismatch to console
156
+ */
157
+ function reportHydrationMismatch(mismatch, options = {}) {
158
+ if (!mismatch) return;
159
+
160
+ const { component, diff, attrMismatches } = mismatch;
161
+
162
+ console.group('\x1b[33m⚠️ MasterController Hydration Mismatch\x1b[0m');
163
+ console.log(`\x1b[36mComponent:\x1b[0m ${component}`);
164
+
165
+ // Attribute mismatches
166
+ if (attrMismatches.length > 0) {
167
+ console.log('\n\x1b[33mAttribute Mismatches:\x1b[0m');
168
+ attrMismatches.forEach(attr => {
169
+ console.log(` ${attr.attribute}:`);
170
+ console.log(` \x1b[31mServer: ${attr.server}\x1b[0m`);
171
+ console.log(` \x1b[32mClient: ${attr.client}\x1b[0m`);
172
+ });
173
+ }
174
+
175
+ // HTML content mismatches
176
+ if (diff.length > 0) {
177
+ console.log('\n\x1b[33mHTML Diff:\x1b[0m');
178
+ console.log(formatDiffForConsole(diff));
179
+ }
180
+
181
+ // Suggestions
182
+ console.log('\n\x1b[36mPossible Causes:\x1b[0m');
183
+ console.log(' 1. Component state differs between server and client');
184
+ console.log(' 2. Conditional rendering based on client-only APIs (window, navigator, etc.)');
185
+ console.log(' 3. Missing or incorrect attributes in client-side render');
186
+ console.log(' 4. Random values or timestamps generated during render');
187
+ console.log(' 5. Missing data-ssr guard in connectedCallback');
188
+
189
+ console.log('\n\x1b[36mSuggestions:\x1b[0m');
190
+ console.log(' • Ensure server and client render with same props/state');
191
+ console.log(' • Use typeof window !== "undefined" checks for browser APIs');
192
+ console.log(' • Avoid random values or Date.now() in render logic');
193
+ console.log(' • Verify data-ssr attribute is present on server-rendered elements');
194
+
195
+ console.log('\n\x1b[34mLearn more:\x1b[0m https://mastercontroller.dev/docs/hydration#mismatches');
196
+ console.groupEnd();
197
+
198
+ // Log to monitoring service
199
+ if (typeof window !== 'undefined' && window.masterControllerErrorReporter) {
200
+ window.masterControllerErrorReporter({
201
+ type: 'hydration-mismatch',
202
+ component: mismatch.component,
203
+ diffCount: diff.length,
204
+ attrMismatchCount: attrMismatches.length
205
+ });
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Scan all SSR components for hydration mismatches
211
+ */
212
+ function scanForHydrationMismatches(options = {}) {
213
+ if (!isDevelopment) return;
214
+
215
+ const ssrElements = document.querySelectorAll('[data-ssr]');
216
+ const mismatches = [];
217
+
218
+ ssrElements.forEach(element => {
219
+ const componentName = element.tagName.toLowerCase();
220
+ const mismatch = detectHydrationMismatch(element, componentName, options);
221
+
222
+ if (mismatch) {
223
+ mismatches.push(mismatch);
224
+ reportHydrationMismatch(mismatch, options);
225
+ }
226
+ });
227
+
228
+ if (mismatches.length === 0 && options.verbose) {
229
+ console.log('\x1b[32m✓ No hydration mismatches detected\x1b[0m');
230
+ }
231
+
232
+ return mismatches;
233
+ }
234
+
235
+ /**
236
+ * Enable automatic hydration mismatch detection
237
+ */
238
+ function enableHydrationMismatchDetection(options = {}) {
239
+ if (!isDevelopment) return;
240
+
241
+ // Run check after hydration completes
242
+ if (typeof window !== 'undefined') {
243
+ window.addEventListener('load', () => {
244
+ setTimeout(() => {
245
+ scanForHydrationMismatches(options);
246
+ }, options.delay || 1000);
247
+ });
248
+ }
249
+ }
250
+
251
+ // Auto-enable in development
252
+ if (typeof window !== 'undefined' && isDevelopment) {
253
+ enableHydrationMismatchDetection({
254
+ verbose: localStorage.getItem('mc-hydration-debug') === 'true'
255
+ });
256
+ }
257
+
258
+ module.exports = {
259
+ detectHydrationMismatch,
260
+ reportHydrationMismatch,
261
+ scanForHydrationMismatches,
262
+ enableHydrationMismatchDetection,
263
+ generateDiff,
264
+ compareAttributes
265
+ };