@memberjunction/react-runtime
Version:
Platform-agnostic React component runtime for MemberJunction. Provides core compilation, registry, and execution capabilities for React components in any JavaScript environment.
315 lines (280 loc) • 9.56 kB
text/typescript
/**
* @fileoverview Component error analysis utilities
* Provides methods to analyze component errors and identify failed components
* @module @memberjunction/react-runtime/utilities
*/
/**
* Information about a failed component
*/
export interface FailedComponentInfo {
/** Component name that failed */
componentName: string;
/** Error type (e.g., 'not_defined', 'render_error', 'property_error') */
errorType: string;
/** Original error message */
errorMessage: string;
/** Line number if available */
lineNumber?: number;
/** Additional context */
context?: string;
}
/**
* Analyzes component errors to provide detailed failure information
*/
export class ComponentErrorAnalyzer {
/**
* Common error patterns for component failures
*/
private static readonly ERROR_PATTERNS = [
{
// Component is not defined
pattern: /ReferenceError: (\w+) is not defined/,
errorType: 'not_defined',
extractComponent: (match: RegExpMatchArray) => match[1]
},
{
// Cannot read property of undefined (component reference)
pattern: /Cannot read propert(?:y|ies) '(\w+)' of undefined/,
errorType: 'property_error',
extractComponent: (match: RegExpMatchArray) => match[1]
},
{
// Component render errors
pattern: /(\w+)\(\.\.\.\): Nothing was returned from render/,
errorType: 'render_error',
extractComponent: (match: RegExpMatchArray) => match[1]
},
{
// Component in stack trace
pattern: /at (\w+Component\w*)/,
errorType: 'stack_trace',
extractComponent: (match: RegExpMatchArray) => match[1]
},
{
// React component errors
pattern: /Error: Unable to find node on an unmounted component/,
errorType: 'unmounted_component',
extractComponent: () => null // Need to look at stack trace
},
{
// Hook errors
pattern: /Invalid hook call.*component (\w+)/,
errorType: 'invalid_hook',
extractComponent: (match: RegExpMatchArray) => match[1]
},
{
// Type errors in components
pattern: /TypeError:.*in (\w+) \(at/,
errorType: 'type_error',
extractComponent: (match: RegExpMatchArray) => match[1]
},
{
// Missing imports/components
pattern: /Module not found: Error: Can't resolve '\.\/(\w+)'/,
errorType: 'missing_import',
extractComponent: (match: RegExpMatchArray) => match[1]
},
{
// Component is not a function
pattern: /(\w+) is not a function/,
errorType: 'not_a_function',
extractComponent: (match: RegExpMatchArray) => match[1]
},
{
// Minified React error with component hint
pattern: /Minified React error.*Visit.*for the full message.*component[: ](\w+)/s,
errorType: 'react_error',
extractComponent: (match: RegExpMatchArray) => match[1]
}
];
/**
* Analyzes error messages to identify which components failed
* @param errors Array of error messages
* @returns Array of failed component names
*/
static identifyFailedComponents(errors: string[]): string[] {
const failedComponents = new Set<string>();
for (const error of errors) {
const components = this.extractComponentsFromError(error);
components.forEach(comp => failedComponents.add(comp));
}
return Array.from(failedComponents);
}
/**
* Analyzes errors and returns detailed information about failures
* @param errors Array of error messages
* @returns Array of detailed failure information
*/
static analyzeComponentErrors(errors: string[]): FailedComponentInfo[] {
const failures: FailedComponentInfo[] = [];
for (const error of errors) {
const failureInfo = this.analyzeError(error);
failures.push(...failureInfo);
}
// Remove duplicates based on component name and error type
const uniqueFailures = new Map<string, FailedComponentInfo>();
failures.forEach(failure => {
const key = `${failure.componentName}-${failure.errorType}`;
if (!uniqueFailures.has(key)) {
uniqueFailures.set(key, failure);
}
});
return Array.from(uniqueFailures.values());
}
/**
* Extract component names from a single error message
*/
private static extractComponentsFromError(error: string): string[] {
const components: string[] = [];
for (const errorPattern of this.ERROR_PATTERNS) {
const match = error.match(errorPattern.pattern);
if (match) {
const componentName = errorPattern.extractComponent(match);
if (componentName && this.isLikelyComponentName(componentName)) {
components.push(componentName);
}
}
}
// Also check for components in stack traces
const stackComponents = this.extractComponentsFromStackTrace(error);
components.push(...stackComponents);
return components;
}
/**
* Analyze a single error and return detailed information
*/
private static analyzeError(error: string): FailedComponentInfo[] {
const failures: FailedComponentInfo[] = [];
for (const errorPattern of this.ERROR_PATTERNS) {
const match = error.match(errorPattern.pattern);
if (match) {
const componentName = errorPattern.extractComponent(match);
if (componentName && this.isLikelyComponentName(componentName)) {
failures.push({
componentName,
errorType: errorPattern.errorType,
errorMessage: error,
lineNumber: this.extractLineNumber(error),
context: this.extractContext(error)
});
}
}
}
// If no specific pattern matched, try to extract from stack trace
if (failures.length === 0) {
const stackComponents = this.extractComponentsFromStackTrace(error);
stackComponents.forEach(componentName => {
failures.push({
componentName,
errorType: 'unknown',
errorMessage: error,
lineNumber: this.extractLineNumber(error)
});
});
}
return failures;
}
/**
* Extract component names from stack trace
*/
private static extractComponentsFromStackTrace(error: string): string[] {
const components: string[] = [];
// Look for React component patterns in stack traces
const stackPatterns = [
/at (\w+Component\w*)/g,
/at (\w+)\s*\(/g,
/in (\w+)\s*\(at/g,
/in (\w+)\s*\(created by/g
];
for (const pattern of stackPatterns) {
let match;
while ((match = pattern.exec(error)) !== null) {
const name = match[1];
if (this.isLikelyComponentName(name)) {
components.push(name);
}
}
}
return [...new Set(components)]; // Remove duplicates
}
/**
* Check if a string is likely to be a React component name
*/
private static isLikelyComponentName(name: string): boolean {
// Component names typically:
// - Start with uppercase letter
// - Are not JavaScript built-ins
// - Are not common non-component names
const jsBuiltins = new Set([
'Object', 'Array', 'String', 'Number', 'Boolean', 'Function',
'Promise', 'Error', 'TypeError', 'ReferenceError', 'SyntaxError',
'undefined', 'null', 'console', 'window', 'document'
]);
const nonComponents = new Set([
'render', 'setState', 'forceUpdate', 'props', 'state', 'context',
'componentDidMount', 'componentWillUnmount', 'useEffect', 'useState'
]);
return (
name.length > 0 &&
/^[A-Z]/.test(name) && // Starts with uppercase
!jsBuiltins.has(name) &&
!nonComponents.has(name) &&
!/^use[A-Z]/.test(name) // Not a hook
);
}
/**
* Extract line number from error message
*/
private static extractLineNumber(error: string): number | undefined {
// Look for patterns like ":12:34" or "line 12"
const patterns = [
/:(\d+):\d+/,
/line (\d+)/i,
/Line (\d+)/
];
for (const pattern of patterns) {
const match = error.match(pattern);
if (match) {
return parseInt(match[1], 10);
}
}
return undefined;
}
/**
* Extract additional context from error
*/
private static extractContext(error: string): string | undefined {
// Extract file names or additional context
const fileMatch = error.match(/\(at ([^)]+)\)/);
if (fileMatch) {
return fileMatch[1];
}
// Extract "created by" information
const createdByMatch = error.match(/created by (\w+)/);
if (createdByMatch) {
return `Created by ${createdByMatch[1]}`;
}
return undefined;
}
/**
* Format error analysis results for logging
*/
static formatAnalysisResults(failures: FailedComponentInfo[]): string {
if (failures.length === 0) {
return 'No component failures detected';
}
let result = `Detected ${failures.length} component failure(s):\n`;
failures.forEach((failure, index) => {
result += `\n${index + 1}. Component: ${failure.componentName}\n`;
result += ` Error Type: ${failure.errorType}\n`;
if (failure.lineNumber) {
result += ` Line: ${failure.lineNumber}\n`;
}
if (failure.context) {
result += ` Context: ${failure.context}\n`;
}
result += ` Message: ${failure.errorMessage.substring(0, 200)}${failure.errorMessage.length > 200 ? '...' : ''}\n`;
});
return result;
}
}