mastercontroller 1.2.11 → 1.2.13
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/CSPConfig.js +319 -0
- package/EventHandlerValidator.js +464 -0
- package/MasterAction.js +296 -72
- package/MasterBackendErrorHandler.js +769 -0
- package/MasterBenchmark.js +89 -0
- package/MasterBuildOptimizer.js +376 -0
- package/MasterBundleAnalyzer.js +108 -0
- package/MasterCache.js +400 -0
- package/MasterControl.js +77 -7
- package/MasterErrorHandler.js +487 -0
- package/MasterErrorLogger.js +360 -0
- package/MasterErrorMiddleware.js +407 -0
- package/MasterHtml.js +101 -14
- package/MasterMemoryMonitor.js +188 -0
- package/MasterProfiler.js +409 -0
- package/MasterRouter.js +273 -66
- package/MasterSanitizer.js +429 -0
- package/MasterTemplate.js +96 -3
- package/MasterValidator.js +546 -0
- package/README.md +0 -44
- package/SecurityMiddleware.js +486 -0
- package/SessionSecurity.js +416 -0
- package/package.json +2 -2
- package/ssr/ErrorBoundary.js +353 -0
- package/ssr/HTMLUtils.js +15 -0
- package/ssr/HydrationMismatch.js +265 -0
- package/ssr/PerformanceMonitor.js +233 -0
- package/ssr/SSRErrorHandler.js +273 -0
- package/ssr/hydration-client.js +93 -0
- package/ssr/runtime-ssr.cjs +553 -0
- package/ssr/ssr-shims.js +73 -0
- package/examples/FileServingExample.js +0 -88
|
@@ -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, '&')
|
|
279
|
+
.replace(/</g, '<')
|
|
280
|
+
.replace(/>/g, '>')
|
|
281
|
+
.replace(/"/g, '"')
|
|
282
|
+
.replace(/'/g, ''');
|
|
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 };
|
package/ssr/HTMLUtils.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deprecated: HTMLUtils.js
|
|
3
|
+
* Enhance SSR now compiles native web components directly to HTML.
|
|
4
|
+
* This file remains as a no-op compatibility stub so any legacy references do not break.
|
|
5
|
+
*/
|
|
6
|
+
class HTMLUtils {
|
|
7
|
+
static escapeAttr(v) { return String(v ?? ''); }
|
|
8
|
+
static unescapeAttr(v) { return v; }
|
|
9
|
+
static encodeData(v) { return this.escapeAttr(v); }
|
|
10
|
+
static decodeData(v) { return this.unescapeAttr(v); }
|
|
11
|
+
static dataAttr(name, value) { return `${name}="${this.escapeAttr(value)}"`; }
|
|
12
|
+
}
|
|
13
|
+
if (typeof module !== 'undefined' && module.exports) module.exports = HTMLUtils;
|
|
14
|
+
if (typeof window !== 'undefined') window.HTMLUtils = HTMLUtils;
|
|
15
|
+
if (typeof exports !== 'undefined') exports.HTMLUtils = HTMLUtils;
|
|
@@ -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
|
+
};
|