mastercontroller 1.3.6 → 1.3.8
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/.claude/settings.local.json +3 -1
- package/MasterRequest.js +6 -0
- package/error/ErrorBoundary.js +353 -0
- package/error/HydrationMismatch.js +265 -0
- package/error/MasterBackendErrorHandler.js +769 -0
- package/error/MasterError.js +240 -0
- package/error/MasterError.js.tmp +0 -0
- package/error/MasterErrorHandler.js +487 -0
- package/error/MasterErrorLogger.js +360 -0
- package/error/MasterErrorMiddleware.js +407 -0
- package/error/MasterErrorRenderer.js +536 -0
- package/error/MasterErrorRenderer.js.tmp +0 -0
- package/error/SSRErrorHandler.js +273 -0
- package/log/mastercontroller.log +2 -0
- package/package.json +7 -6
- package/test-json-empty-body.js +76 -0
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MasterErrorHandler - Comprehensive error handling system
|
|
3
|
+
* Provides formatted error messages with helpful suggestions and documentation links
|
|
4
|
+
* Version: 1.0.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
// ANSI color codes for terminal output
|
|
10
|
+
const colors = {
|
|
11
|
+
reset: '\x1b[0m',
|
|
12
|
+
bright: '\x1b[1m',
|
|
13
|
+
dim: '\x1b[2m',
|
|
14
|
+
red: '\x1b[31m',
|
|
15
|
+
green: '\x1b[32m',
|
|
16
|
+
yellow: '\x1b[33m',
|
|
17
|
+
blue: '\x1b[34m',
|
|
18
|
+
magenta: '\x1b[35m',
|
|
19
|
+
cyan: '\x1b[36m',
|
|
20
|
+
white: '\x1b[37m',
|
|
21
|
+
bgRed: '\x1b[41m',
|
|
22
|
+
bgYellow: '\x1b[43m',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Error code definitions
|
|
26
|
+
const ERROR_CODES = {
|
|
27
|
+
MC_ERR_EVENT_HANDLER_NOT_FOUND: {
|
|
28
|
+
title: 'Event Handler Not Found',
|
|
29
|
+
docsPath: '/docs/events#handler-not-found',
|
|
30
|
+
severity: 'error'
|
|
31
|
+
},
|
|
32
|
+
MC_ERR_EVENT_SYNTAX_INVALID: {
|
|
33
|
+
title: 'Invalid @event Syntax',
|
|
34
|
+
docsPath: '/docs/events#syntax',
|
|
35
|
+
severity: 'error'
|
|
36
|
+
},
|
|
37
|
+
MC_ERR_COMPONENT_RENDER_FAILED: {
|
|
38
|
+
title: 'Component Render Failed',
|
|
39
|
+
docsPath: '/docs/ssr#render-errors',
|
|
40
|
+
severity: 'error'
|
|
41
|
+
},
|
|
42
|
+
MC_ERR_TEMPRENDER_MISSING: {
|
|
43
|
+
title: 'Missing tempRender() Method',
|
|
44
|
+
docsPath: '/docs/components#temprender',
|
|
45
|
+
severity: 'warning'
|
|
46
|
+
},
|
|
47
|
+
MC_ERR_DUPLICATE_ELEMENT: {
|
|
48
|
+
title: 'Duplicate Custom Element Registration',
|
|
49
|
+
docsPath: '/docs/components#duplicate-names',
|
|
50
|
+
severity: 'warning'
|
|
51
|
+
},
|
|
52
|
+
MC_ERR_HYDRATION_MISMATCH: {
|
|
53
|
+
title: 'Hydration Mismatch Detected',
|
|
54
|
+
docsPath: '/docs/hydration#mismatches',
|
|
55
|
+
severity: 'warning'
|
|
56
|
+
},
|
|
57
|
+
MC_ERR_SLOW_RENDER: {
|
|
58
|
+
title: 'Slow Component Render',
|
|
59
|
+
docsPath: '/docs/performance',
|
|
60
|
+
severity: 'warning'
|
|
61
|
+
},
|
|
62
|
+
MC_ERR_MANIFEST_PARSE: {
|
|
63
|
+
title: 'Event Manifest Parse Error',
|
|
64
|
+
docsPath: '/docs/events#manifest-errors',
|
|
65
|
+
severity: 'error'
|
|
66
|
+
},
|
|
67
|
+
MC_ERR_MODULE_LOAD: {
|
|
68
|
+
title: 'Module Load Failed',
|
|
69
|
+
docsPath: '/docs/troubleshooting#module-errors',
|
|
70
|
+
severity: 'error'
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Levenshtein distance for "Did you mean?" suggestions
|
|
76
|
+
*/
|
|
77
|
+
function levenshteinDistance(str1, str2) {
|
|
78
|
+
const len1 = str1.length;
|
|
79
|
+
const len2 = str2.length;
|
|
80
|
+
const matrix = Array(len1 + 1).fill(null).map(() => Array(len2 + 1).fill(0));
|
|
81
|
+
|
|
82
|
+
for (let i = 0; i <= len1; i++) matrix[i][0] = i;
|
|
83
|
+
for (let j = 0; j <= len2; j++) matrix[0][j] = j;
|
|
84
|
+
|
|
85
|
+
for (let i = 1; i <= len1; i++) {
|
|
86
|
+
for (let j = 1; j <= len2; j++) {
|
|
87
|
+
const cost = str1[i - 1] === str2[j - 1] ? 0 : 1;
|
|
88
|
+
matrix[i][j] = Math.min(
|
|
89
|
+
matrix[i - 1][j] + 1,
|
|
90
|
+
matrix[i][j - 1] + 1,
|
|
91
|
+
matrix[i - 1][j - 1] + cost
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return matrix[len1][len2];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Find similar strings for suggestions
|
|
101
|
+
*/
|
|
102
|
+
function findSimilarStrings(target, candidates, maxSuggestions = 3) {
|
|
103
|
+
if (!target || !candidates || candidates.length === 0) return [];
|
|
104
|
+
|
|
105
|
+
const withDistances = candidates
|
|
106
|
+
.map(candidate => ({
|
|
107
|
+
value: candidate,
|
|
108
|
+
distance: levenshteinDistance(target.toLowerCase(), candidate.toLowerCase())
|
|
109
|
+
}))
|
|
110
|
+
.filter(item => item.distance <= 3) // Only suggest if reasonably close
|
|
111
|
+
.sort((a, b) => a.distance - b.distance);
|
|
112
|
+
|
|
113
|
+
return withDistances.slice(0, maxSuggestions).map(item => item.value);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Extract line number from stack trace
|
|
118
|
+
*/
|
|
119
|
+
function extractLineNumber(stack, filePath) {
|
|
120
|
+
if (!stack || !filePath) return null;
|
|
121
|
+
|
|
122
|
+
const lines = stack.split('\n');
|
|
123
|
+
for (const line of lines) {
|
|
124
|
+
if (line.includes(filePath)) {
|
|
125
|
+
const match = line.match(/:(\d+):(\d+)/);
|
|
126
|
+
if (match) return parseInt(match[1], 10);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Get relative path from project root
|
|
134
|
+
*/
|
|
135
|
+
function getRelativePath(absolutePath) {
|
|
136
|
+
if (!absolutePath) return null;
|
|
137
|
+
const cwd = process.cwd();
|
|
138
|
+
return path.relative(cwd, absolutePath);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
class MasterControllerError extends Error {
|
|
142
|
+
constructor(options = {}) {
|
|
143
|
+
super(options.message || 'An error occurred');
|
|
144
|
+
|
|
145
|
+
this.name = 'MasterControllerError';
|
|
146
|
+
this.code = options.code || 'MC_ERR_UNKNOWN';
|
|
147
|
+
this.component = options.component || null;
|
|
148
|
+
this.file = options.file || null;
|
|
149
|
+
this.line = options.line || null;
|
|
150
|
+
this.handler = options.handler || null;
|
|
151
|
+
this.expected = options.expected || null;
|
|
152
|
+
this.suggestions = options.suggestions || [];
|
|
153
|
+
this.details = options.details || null;
|
|
154
|
+
this.context = options.context || {};
|
|
155
|
+
this.originalError = options.originalError || null;
|
|
156
|
+
|
|
157
|
+
// Get error metadata from code
|
|
158
|
+
this.metadata = ERROR_CODES[this.code] || {
|
|
159
|
+
title: 'Unknown Error',
|
|
160
|
+
docsPath: '/docs',
|
|
161
|
+
severity: 'error'
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// Build docs URL
|
|
165
|
+
this.docsUrl = options.docsUrl || this._buildDocsUrl();
|
|
166
|
+
|
|
167
|
+
// Extract line number from stack if not provided
|
|
168
|
+
if (!this.line && this.originalError && this.originalError.stack && this.file) {
|
|
169
|
+
this.line = extractLineNumber(this.originalError.stack, this.file);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
Error.captureStackTrace(this, this.constructor);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
_buildDocsUrl() {
|
|
176
|
+
const baseUrl = process.env.MASTER_DOCS_URL || 'https://mastercontroller.dev';
|
|
177
|
+
return baseUrl + this.metadata.docsPath;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Format error for terminal output with colors
|
|
182
|
+
*/
|
|
183
|
+
format() {
|
|
184
|
+
const { bright, red, yellow, cyan, blue, green, dim, reset } = colors;
|
|
185
|
+
const isError = this.metadata.severity === 'error';
|
|
186
|
+
const icon = isError ? '❌' : '⚠️';
|
|
187
|
+
const titleColor = isError ? red : yellow;
|
|
188
|
+
|
|
189
|
+
let output = '\n';
|
|
190
|
+
output += `${titleColor}${bright}${icon} MasterController ${isError ? 'Error' : 'Warning'}: ${this.metadata.title}${reset}\n`;
|
|
191
|
+
output += `${dim}${'─'.repeat(80)}${reset}\n\n`;
|
|
192
|
+
|
|
193
|
+
// Component info
|
|
194
|
+
if (this.component) {
|
|
195
|
+
output += `${cyan}Component:${reset} <${this.component}>\n`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// File location
|
|
199
|
+
if (this.file) {
|
|
200
|
+
const relativePath = getRelativePath(this.file);
|
|
201
|
+
const location = this.line ? `${relativePath}:${this.line}` : relativePath;
|
|
202
|
+
output += `${cyan}Location:${reset} ${location}\n`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Handler details
|
|
206
|
+
if (this.handler) {
|
|
207
|
+
output += `${cyan}Handler:${reset} ${this.handler}`;
|
|
208
|
+
if (this.expected) {
|
|
209
|
+
output += ` ${dim}(expected: ${this.expected})${reset}`;
|
|
210
|
+
}
|
|
211
|
+
output += '\n';
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Main message
|
|
215
|
+
if (this.message) {
|
|
216
|
+
output += `\n${this.message}\n`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Details
|
|
220
|
+
if (this.details) {
|
|
221
|
+
output += `\n${dim}${this.details}${reset}\n`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Suggestions
|
|
225
|
+
if (this.suggestions && this.suggestions.length > 0) {
|
|
226
|
+
output += `\n${green}${bright}Did you mean?${reset}\n`;
|
|
227
|
+
this.suggestions.forEach(suggestion => {
|
|
228
|
+
output += ` ${green}→${reset} ${suggestion}\n`;
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Fix instructions
|
|
233
|
+
if (this.file && this.line) {
|
|
234
|
+
output += `\n${blue}${bright}Fix:${reset} Check ${getRelativePath(this.file)}:${this.line}\n`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Original error stack (in development)
|
|
238
|
+
if (this.originalError && process.env.NODE_ENV !== 'production') {
|
|
239
|
+
output += `\n${dim}Original Error:${reset}\n${dim}${this.originalError.stack}${reset}\n`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Documentation link
|
|
243
|
+
output += `\n${blue}${bright}Learn more:${reset} ${this.docsUrl}\n`;
|
|
244
|
+
output += `${dim}${'─'.repeat(80)}${reset}\n`;
|
|
245
|
+
|
|
246
|
+
return output;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Format error for HTML output (development error page)
|
|
251
|
+
*/
|
|
252
|
+
toHTML() {
|
|
253
|
+
const isError = this.metadata.severity === 'error';
|
|
254
|
+
const bgColor = isError ? '#fee' : '#fffbeb';
|
|
255
|
+
const borderColor = isError ? '#f87171' : '#fbbf24';
|
|
256
|
+
const iconColor = isError ? '#dc2626' : '#f59e0b';
|
|
257
|
+
|
|
258
|
+
const relativePath = this.file ? getRelativePath(this.file) : '';
|
|
259
|
+
const location = this.line ? `${relativePath}:${this.line}` : relativePath;
|
|
260
|
+
|
|
261
|
+
return `
|
|
262
|
+
<!DOCTYPE html>
|
|
263
|
+
<html lang="en">
|
|
264
|
+
<head>
|
|
265
|
+
<meta charset="UTF-8">
|
|
266
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
267
|
+
<title>MasterController ${isError ? 'Error' : 'Warning'}</title>
|
|
268
|
+
<style>
|
|
269
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
270
|
+
body {
|
|
271
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
272
|
+
background: #f9fafb;
|
|
273
|
+
padding: 20px;
|
|
274
|
+
color: #1f2937;
|
|
275
|
+
}
|
|
276
|
+
.container {
|
|
277
|
+
max-width: 900px;
|
|
278
|
+
margin: 0 auto;
|
|
279
|
+
background: white;
|
|
280
|
+
border-radius: 8px;
|
|
281
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
282
|
+
overflow: hidden;
|
|
283
|
+
}
|
|
284
|
+
.header {
|
|
285
|
+
background: ${bgColor};
|
|
286
|
+
border-left: 4px solid ${borderColor};
|
|
287
|
+
padding: 24px;
|
|
288
|
+
}
|
|
289
|
+
.header h1 {
|
|
290
|
+
font-size: 24px;
|
|
291
|
+
font-weight: 700;
|
|
292
|
+
color: ${iconColor};
|
|
293
|
+
margin-bottom: 8px;
|
|
294
|
+
}
|
|
295
|
+
.header .subtitle {
|
|
296
|
+
font-size: 18px;
|
|
297
|
+
color: #374151;
|
|
298
|
+
font-weight: 600;
|
|
299
|
+
}
|
|
300
|
+
.content {
|
|
301
|
+
padding: 24px;
|
|
302
|
+
}
|
|
303
|
+
.section {
|
|
304
|
+
margin-bottom: 24px;
|
|
305
|
+
}
|
|
306
|
+
.section-title {
|
|
307
|
+
font-size: 14px;
|
|
308
|
+
font-weight: 600;
|
|
309
|
+
color: #6b7280;
|
|
310
|
+
text-transform: uppercase;
|
|
311
|
+
letter-spacing: 0.05em;
|
|
312
|
+
margin-bottom: 8px;
|
|
313
|
+
}
|
|
314
|
+
.section-content {
|
|
315
|
+
font-size: 16px;
|
|
316
|
+
color: #1f2937;
|
|
317
|
+
line-height: 1.6;
|
|
318
|
+
}
|
|
319
|
+
.code {
|
|
320
|
+
background: #f3f4f6;
|
|
321
|
+
padding: 12px 16px;
|
|
322
|
+
border-radius: 6px;
|
|
323
|
+
font-family: 'Courier New', monospace;
|
|
324
|
+
font-size: 14px;
|
|
325
|
+
overflow-x: auto;
|
|
326
|
+
border-left: 3px solid #3b82f6;
|
|
327
|
+
}
|
|
328
|
+
.suggestions {
|
|
329
|
+
list-style: none;
|
|
330
|
+
}
|
|
331
|
+
.suggestions li {
|
|
332
|
+
background: #ecfdf5;
|
|
333
|
+
padding: 8px 12px;
|
|
334
|
+
margin-bottom: 8px;
|
|
335
|
+
border-radius: 4px;
|
|
336
|
+
border-left: 3px solid #10b981;
|
|
337
|
+
}
|
|
338
|
+
.suggestions li:before {
|
|
339
|
+
content: '→ ';
|
|
340
|
+
color: #10b981;
|
|
341
|
+
font-weight: bold;
|
|
342
|
+
}
|
|
343
|
+
.link {
|
|
344
|
+
display: inline-block;
|
|
345
|
+
background: #3b82f6;
|
|
346
|
+
color: white;
|
|
347
|
+
padding: 10px 20px;
|
|
348
|
+
border-radius: 6px;
|
|
349
|
+
text-decoration: none;
|
|
350
|
+
font-weight: 600;
|
|
351
|
+
margin-top: 16px;
|
|
352
|
+
}
|
|
353
|
+
.link:hover {
|
|
354
|
+
background: #2563eb;
|
|
355
|
+
}
|
|
356
|
+
.stack {
|
|
357
|
+
background: #1f2937;
|
|
358
|
+
color: #f3f4f6;
|
|
359
|
+
padding: 16px;
|
|
360
|
+
border-radius: 6px;
|
|
361
|
+
font-family: 'Courier New', monospace;
|
|
362
|
+
font-size: 12px;
|
|
363
|
+
overflow-x: auto;
|
|
364
|
+
max-height: 300px;
|
|
365
|
+
overflow-y: auto;
|
|
366
|
+
}
|
|
367
|
+
</style>
|
|
368
|
+
</head>
|
|
369
|
+
<body>
|
|
370
|
+
<div class="container">
|
|
371
|
+
<div class="header">
|
|
372
|
+
<h1>${isError ? '❌ Error' : '⚠️ Warning'}</h1>
|
|
373
|
+
<div class="subtitle">${this.escapeHtml(this.metadata.title)}</div>
|
|
374
|
+
</div>
|
|
375
|
+
|
|
376
|
+
<div class="content">
|
|
377
|
+
${this.component ? `
|
|
378
|
+
<div class="section">
|
|
379
|
+
<div class="section-title">Component</div>
|
|
380
|
+
<div class="section-content code"><${this.escapeHtml(this.component)}></div>
|
|
381
|
+
</div>
|
|
382
|
+
` : ''}
|
|
383
|
+
|
|
384
|
+
${this.file ? `
|
|
385
|
+
<div class="section">
|
|
386
|
+
<div class="section-title">Location</div>
|
|
387
|
+
<div class="section-content code">${this.escapeHtml(location)}</div>
|
|
388
|
+
</div>
|
|
389
|
+
` : ''}
|
|
390
|
+
|
|
391
|
+
${this.handler ? `
|
|
392
|
+
<div class="section">
|
|
393
|
+
<div class="section-title">Handler</div>
|
|
394
|
+
<div class="section-content code">${this.escapeHtml(this.handler)}${this.expected ? ` <span style="color: #6b7280;">(expected: ${this.escapeHtml(this.expected)})</span>` : ''}</div>
|
|
395
|
+
</div>
|
|
396
|
+
` : ''}
|
|
397
|
+
|
|
398
|
+
${this.message ? `
|
|
399
|
+
<div class="section">
|
|
400
|
+
<div class="section-title">Message</div>
|
|
401
|
+
<div class="section-content">${this.escapeHtml(this.message)}</div>
|
|
402
|
+
</div>
|
|
403
|
+
` : ''}
|
|
404
|
+
|
|
405
|
+
${this.details ? `
|
|
406
|
+
<div class="section">
|
|
407
|
+
<div class="section-title">Details</div>
|
|
408
|
+
<div class="section-content">${this.escapeHtml(this.details)}</div>
|
|
409
|
+
</div>
|
|
410
|
+
` : ''}
|
|
411
|
+
|
|
412
|
+
${this.suggestions && this.suggestions.length > 0 ? `
|
|
413
|
+
<div class="section">
|
|
414
|
+
<div class="section-title">Did you mean?</div>
|
|
415
|
+
<ul class="suggestions">
|
|
416
|
+
${this.suggestions.map(s => `<li>${this.escapeHtml(s)}</li>`).join('')}
|
|
417
|
+
</ul>
|
|
418
|
+
</div>
|
|
419
|
+
` : ''}
|
|
420
|
+
|
|
421
|
+
${this.originalError && process.env.NODE_ENV !== 'production' ? `
|
|
422
|
+
<div class="section">
|
|
423
|
+
<div class="section-title">Stack Trace</div>
|
|
424
|
+
<pre class="stack">${this.escapeHtml(this.originalError.stack || '')}</pre>
|
|
425
|
+
</div>
|
|
426
|
+
` : ''}
|
|
427
|
+
|
|
428
|
+
<div class="section">
|
|
429
|
+
<a href="${this.docsUrl}" class="link" target="_blank">View Documentation →</a>
|
|
430
|
+
</div>
|
|
431
|
+
</div>
|
|
432
|
+
</div>
|
|
433
|
+
</body>
|
|
434
|
+
</html>
|
|
435
|
+
`.trim();
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Format error for JSON logging
|
|
440
|
+
*/
|
|
441
|
+
toJSON() {
|
|
442
|
+
return {
|
|
443
|
+
name: this.name,
|
|
444
|
+
code: this.code,
|
|
445
|
+
message: this.message,
|
|
446
|
+
severity: this.metadata.severity,
|
|
447
|
+
component: this.component,
|
|
448
|
+
file: this.file ? getRelativePath(this.file) : null,
|
|
449
|
+
line: this.line,
|
|
450
|
+
handler: this.handler,
|
|
451
|
+
expected: this.expected,
|
|
452
|
+
suggestions: this.suggestions,
|
|
453
|
+
details: this.details,
|
|
454
|
+
context: this.context,
|
|
455
|
+
docsUrl: this.docsUrl,
|
|
456
|
+
timestamp: new Date().toISOString(),
|
|
457
|
+
stack: this.stack,
|
|
458
|
+
originalError: this.originalError ? {
|
|
459
|
+
message: this.originalError.message,
|
|
460
|
+
stack: this.originalError.stack
|
|
461
|
+
} : null
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Escape HTML for safe output
|
|
467
|
+
*/
|
|
468
|
+
escapeHtml(str) {
|
|
469
|
+
if (!str) return '';
|
|
470
|
+
return String(str)
|
|
471
|
+
.replace(/&/g, '&')
|
|
472
|
+
.replace(/</g, '<')
|
|
473
|
+
.replace(/>/g, '>')
|
|
474
|
+
.replace(/"/g, '"')
|
|
475
|
+
.replace(/'/g, ''');
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Export utilities
|
|
480
|
+
module.exports = {
|
|
481
|
+
MasterControllerError,
|
|
482
|
+
ERROR_CODES,
|
|
483
|
+
findSimilarStrings,
|
|
484
|
+
levenshteinDistance,
|
|
485
|
+
getRelativePath,
|
|
486
|
+
extractLineNumber
|
|
487
|
+
};
|