@memberjunction/react-runtime 2.71.0 → 2.72.0

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,315 @@
1
+ /**
2
+ * @fileoverview Component error analysis utilities
3
+ * Provides methods to analyze component errors and identify failed components
4
+ * @module @memberjunction/react-runtime/utilities
5
+ */
6
+
7
+ /**
8
+ * Information about a failed component
9
+ */
10
+ export interface FailedComponentInfo {
11
+ /** Component name that failed */
12
+ componentName: string;
13
+ /** Error type (e.g., 'not_defined', 'render_error', 'property_error') */
14
+ errorType: string;
15
+ /** Original error message */
16
+ errorMessage: string;
17
+ /** Line number if available */
18
+ lineNumber?: number;
19
+ /** Additional context */
20
+ context?: string;
21
+ }
22
+
23
+ /**
24
+ * Analyzes component errors to provide detailed failure information
25
+ */
26
+ export class ComponentErrorAnalyzer {
27
+ /**
28
+ * Common error patterns for component failures
29
+ */
30
+ private static readonly ERROR_PATTERNS = [
31
+ {
32
+ // Component is not defined
33
+ pattern: /ReferenceError: (\w+) is not defined/,
34
+ errorType: 'not_defined',
35
+ extractComponent: (match: RegExpMatchArray) => match[1]
36
+ },
37
+ {
38
+ // Cannot read property of undefined (component reference)
39
+ pattern: /Cannot read propert(?:y|ies) '(\w+)' of undefined/,
40
+ errorType: 'property_error',
41
+ extractComponent: (match: RegExpMatchArray) => match[1]
42
+ },
43
+ {
44
+ // Component render errors
45
+ pattern: /(\w+)\(\.\.\.\): Nothing was returned from render/,
46
+ errorType: 'render_error',
47
+ extractComponent: (match: RegExpMatchArray) => match[1]
48
+ },
49
+ {
50
+ // Component in stack trace
51
+ pattern: /at (\w+Component\w*)/,
52
+ errorType: 'stack_trace',
53
+ extractComponent: (match: RegExpMatchArray) => match[1]
54
+ },
55
+ {
56
+ // React component errors
57
+ pattern: /Error: Unable to find node on an unmounted component/,
58
+ errorType: 'unmounted_component',
59
+ extractComponent: () => null // Need to look at stack trace
60
+ },
61
+ {
62
+ // Hook errors
63
+ pattern: /Invalid hook call.*component (\w+)/,
64
+ errorType: 'invalid_hook',
65
+ extractComponent: (match: RegExpMatchArray) => match[1]
66
+ },
67
+ {
68
+ // Type errors in components
69
+ pattern: /TypeError:.*in (\w+) \(at/,
70
+ errorType: 'type_error',
71
+ extractComponent: (match: RegExpMatchArray) => match[1]
72
+ },
73
+ {
74
+ // Missing imports/components
75
+ pattern: /Module not found: Error: Can't resolve '\.\/(\w+)'/,
76
+ errorType: 'missing_import',
77
+ extractComponent: (match: RegExpMatchArray) => match[1]
78
+ },
79
+ {
80
+ // Component is not a function
81
+ pattern: /(\w+) is not a function/,
82
+ errorType: 'not_a_function',
83
+ extractComponent: (match: RegExpMatchArray) => match[1]
84
+ },
85
+ {
86
+ // Minified React error with component hint
87
+ pattern: /Minified React error.*Visit.*for the full message.*component[: ](\w+)/s,
88
+ errorType: 'react_error',
89
+ extractComponent: (match: RegExpMatchArray) => match[1]
90
+ }
91
+ ];
92
+
93
+ /**
94
+ * Analyzes error messages to identify which components failed
95
+ * @param errors Array of error messages
96
+ * @returns Array of failed component names
97
+ */
98
+ static identifyFailedComponents(errors: string[]): string[] {
99
+ const failedComponents = new Set<string>();
100
+
101
+ for (const error of errors) {
102
+ const components = this.extractComponentsFromError(error);
103
+ components.forEach(comp => failedComponents.add(comp));
104
+ }
105
+
106
+ return Array.from(failedComponents);
107
+ }
108
+
109
+ /**
110
+ * Analyzes errors and returns detailed information about failures
111
+ * @param errors Array of error messages
112
+ * @returns Array of detailed failure information
113
+ */
114
+ static analyzeComponentErrors(errors: string[]): FailedComponentInfo[] {
115
+ const failures: FailedComponentInfo[] = [];
116
+
117
+ for (const error of errors) {
118
+ const failureInfo = this.analyzeError(error);
119
+ failures.push(...failureInfo);
120
+ }
121
+
122
+ // Remove duplicates based on component name and error type
123
+ const uniqueFailures = new Map<string, FailedComponentInfo>();
124
+ failures.forEach(failure => {
125
+ const key = `${failure.componentName}-${failure.errorType}`;
126
+ if (!uniqueFailures.has(key)) {
127
+ uniqueFailures.set(key, failure);
128
+ }
129
+ });
130
+
131
+ return Array.from(uniqueFailures.values());
132
+ }
133
+
134
+ /**
135
+ * Extract component names from a single error message
136
+ */
137
+ private static extractComponentsFromError(error: string): string[] {
138
+ const components: string[] = [];
139
+
140
+ for (const errorPattern of this.ERROR_PATTERNS) {
141
+ const match = error.match(errorPattern.pattern);
142
+ if (match) {
143
+ const componentName = errorPattern.extractComponent(match);
144
+ if (componentName && this.isLikelyComponentName(componentName)) {
145
+ components.push(componentName);
146
+ }
147
+ }
148
+ }
149
+
150
+ // Also check for components in stack traces
151
+ const stackComponents = this.extractComponentsFromStackTrace(error);
152
+ components.push(...stackComponents);
153
+
154
+ return components;
155
+ }
156
+
157
+ /**
158
+ * Analyze a single error and return detailed information
159
+ */
160
+ private static analyzeError(error: string): FailedComponentInfo[] {
161
+ const failures: FailedComponentInfo[] = [];
162
+
163
+ for (const errorPattern of this.ERROR_PATTERNS) {
164
+ const match = error.match(errorPattern.pattern);
165
+ if (match) {
166
+ const componentName = errorPattern.extractComponent(match);
167
+ if (componentName && this.isLikelyComponentName(componentName)) {
168
+ failures.push({
169
+ componentName,
170
+ errorType: errorPattern.errorType,
171
+ errorMessage: error,
172
+ lineNumber: this.extractLineNumber(error),
173
+ context: this.extractContext(error)
174
+ });
175
+ }
176
+ }
177
+ }
178
+
179
+ // If no specific pattern matched, try to extract from stack trace
180
+ if (failures.length === 0) {
181
+ const stackComponents = this.extractComponentsFromStackTrace(error);
182
+ stackComponents.forEach(componentName => {
183
+ failures.push({
184
+ componentName,
185
+ errorType: 'unknown',
186
+ errorMessage: error,
187
+ lineNumber: this.extractLineNumber(error)
188
+ });
189
+ });
190
+ }
191
+
192
+ return failures;
193
+ }
194
+
195
+ /**
196
+ * Extract component names from stack trace
197
+ */
198
+ private static extractComponentsFromStackTrace(error: string): string[] {
199
+ const components: string[] = [];
200
+
201
+ // Look for React component patterns in stack traces
202
+ const stackPatterns = [
203
+ /at (\w+Component\w*)/g,
204
+ /at (\w+)\s*\(/g,
205
+ /in (\w+)\s*\(at/g,
206
+ /in (\w+)\s*\(created by/g
207
+ ];
208
+
209
+ for (const pattern of stackPatterns) {
210
+ let match;
211
+ while ((match = pattern.exec(error)) !== null) {
212
+ const name = match[1];
213
+ if (this.isLikelyComponentName(name)) {
214
+ components.push(name);
215
+ }
216
+ }
217
+ }
218
+
219
+ return [...new Set(components)]; // Remove duplicates
220
+ }
221
+
222
+ /**
223
+ * Check if a string is likely to be a React component name
224
+ */
225
+ private static isLikelyComponentName(name: string): boolean {
226
+ // Component names typically:
227
+ // - Start with uppercase letter
228
+ // - Are not JavaScript built-ins
229
+ // - Are not common non-component names
230
+
231
+ const jsBuiltins = new Set([
232
+ 'Object', 'Array', 'String', 'Number', 'Boolean', 'Function',
233
+ 'Promise', 'Error', 'TypeError', 'ReferenceError', 'SyntaxError',
234
+ 'undefined', 'null', 'console', 'window', 'document'
235
+ ]);
236
+
237
+ const nonComponents = new Set([
238
+ 'render', 'setState', 'forceUpdate', 'props', 'state', 'context',
239
+ 'componentDidMount', 'componentWillUnmount', 'useEffect', 'useState'
240
+ ]);
241
+
242
+ return (
243
+ name.length > 0 &&
244
+ /^[A-Z]/.test(name) && // Starts with uppercase
245
+ !jsBuiltins.has(name) &&
246
+ !nonComponents.has(name) &&
247
+ !/^use[A-Z]/.test(name) // Not a hook
248
+ );
249
+ }
250
+
251
+ /**
252
+ * Extract line number from error message
253
+ */
254
+ private static extractLineNumber(error: string): number | undefined {
255
+ // Look for patterns like ":12:34" or "line 12"
256
+ const patterns = [
257
+ /:(\d+):\d+/,
258
+ /line (\d+)/i,
259
+ /Line (\d+)/
260
+ ];
261
+
262
+ for (const pattern of patterns) {
263
+ const match = error.match(pattern);
264
+ if (match) {
265
+ return parseInt(match[1], 10);
266
+ }
267
+ }
268
+
269
+ return undefined;
270
+ }
271
+
272
+ /**
273
+ * Extract additional context from error
274
+ */
275
+ private static extractContext(error: string): string | undefined {
276
+ // Extract file names or additional context
277
+ const fileMatch = error.match(/\(at ([^)]+)\)/);
278
+ if (fileMatch) {
279
+ return fileMatch[1];
280
+ }
281
+
282
+ // Extract "created by" information
283
+ const createdByMatch = error.match(/created by (\w+)/);
284
+ if (createdByMatch) {
285
+ return `Created by ${createdByMatch[1]}`;
286
+ }
287
+
288
+ return undefined;
289
+ }
290
+
291
+ /**
292
+ * Format error analysis results for logging
293
+ */
294
+ static formatAnalysisResults(failures: FailedComponentInfo[]): string {
295
+ if (failures.length === 0) {
296
+ return 'No component failures detected';
297
+ }
298
+
299
+ let result = `Detected ${failures.length} component failure(s):\n`;
300
+
301
+ failures.forEach((failure, index) => {
302
+ result += `\n${index + 1}. Component: ${failure.componentName}\n`;
303
+ result += ` Error Type: ${failure.errorType}\n`;
304
+ if (failure.lineNumber) {
305
+ result += ` Line: ${failure.lineNumber}\n`;
306
+ }
307
+ if (failure.context) {
308
+ result += ` Context: ${failure.context}\n`;
309
+ }
310
+ result += ` Message: ${failure.errorMessage.substring(0, 200)}${failure.errorMessage.length > 200 ? '...' : ''}\n`;
311
+ });
312
+
313
+ return result;
314
+ }
315
+ }
@@ -5,4 +5,6 @@
5
5
 
6
6
  export * from './runtime-utilities';
7
7
  export * from './component-styles';
8
- export * from './standard-libraries';
8
+ export * from './standard-libraries';
9
+ export * from './library-loader';
10
+ export * from './component-error-analyzer';
@@ -0,0 +1,307 @@
1
+ /**
2
+ * @fileoverview Library loading utilities for React runtime
3
+ * Provides methods to load and manage external JavaScript libraries and CSS
4
+ * @module @memberjunction/react-runtime/utilities
5
+ */
6
+
7
+ import {
8
+ STANDARD_LIBRARY_URLS,
9
+ StandardLibraries,
10
+ getCoreLibraryUrls,
11
+ getUILibraryUrls,
12
+ getCSSUrls
13
+ } from './standard-libraries';
14
+
15
+ /**
16
+ * Represents a loaded script or CSS resource
17
+ */
18
+ interface LoadedResource {
19
+ element: HTMLScriptElement | HTMLLinkElement;
20
+ promise: Promise<any>;
21
+ }
22
+
23
+ /**
24
+ * Options for loading libraries
25
+ */
26
+ export interface LibraryLoadOptions {
27
+ /** Load core libraries (lodash, d3, Chart.js, dayjs) */
28
+ loadCore?: boolean;
29
+ /** Load UI libraries (antd, React Bootstrap) */
30
+ loadUI?: boolean;
31
+ /** Load CSS files for UI libraries */
32
+ loadCSS?: boolean;
33
+ /** Custom library URLs to load */
34
+ customLibraries?: { url: string; globalName: string }[];
35
+ }
36
+
37
+ /**
38
+ * Result of loading libraries
39
+ */
40
+ export interface LibraryLoadResult {
41
+ React: any;
42
+ ReactDOM: any;
43
+ Babel: any;
44
+ libraries: StandardLibraries;
45
+ }
46
+
47
+ /**
48
+ * Library loader class for managing external script loading
49
+ */
50
+ export class LibraryLoader {
51
+ private static loadedResources = new Map<string, LoadedResource>();
52
+
53
+ /**
54
+ * Load all standard libraries (core + UI + CSS)
55
+ * This is the main method that should be used by test harness and Angular wrapper
56
+ */
57
+ static async loadAllLibraries(): Promise<LibraryLoadResult> {
58
+ return this.loadLibraries({
59
+ loadCore: true,
60
+ loadUI: true,
61
+ loadCSS: true
62
+ });
63
+ }
64
+
65
+ /**
66
+ * Load libraries with specific options
67
+ */
68
+ static async loadLibraries(options: LibraryLoadOptions): Promise<LibraryLoadResult> {
69
+ const {
70
+ loadCore = true,
71
+ loadUI = true,
72
+ loadCSS = true,
73
+ customLibraries = []
74
+ } = options;
75
+
76
+ // Load React ecosystem first
77
+ const [React, ReactDOM, Babel] = await Promise.all([
78
+ this.loadScript(STANDARD_LIBRARY_URLS.REACT, 'React'),
79
+ this.loadScript(STANDARD_LIBRARY_URLS.REACT_DOM, 'ReactDOM'),
80
+ this.loadScript(STANDARD_LIBRARY_URLS.BABEL, 'Babel')
81
+ ]);
82
+
83
+ // Load CSS files if requested (non-blocking)
84
+ if (loadCSS) {
85
+ getCSSUrls().forEach(url => this.loadCSS(url));
86
+ }
87
+
88
+ // Prepare library loading promises
89
+ const libraryPromises: Promise<any>[] = [];
90
+ const libraryNames: string[] = [];
91
+
92
+ // Core libraries
93
+ if (loadCore) {
94
+ const coreUrls = getCoreLibraryUrls();
95
+ coreUrls.forEach(url => {
96
+ const name = this.getLibraryNameFromUrl(url);
97
+ libraryNames.push(name);
98
+ libraryPromises.push(this.loadScript(url, name));
99
+ });
100
+ }
101
+
102
+ // UI libraries
103
+ if (loadUI) {
104
+ const uiUrls = getUILibraryUrls();
105
+ uiUrls.forEach(url => {
106
+ const name = this.getLibraryNameFromUrl(url);
107
+ libraryNames.push(name);
108
+ libraryPromises.push(this.loadScript(url, name));
109
+ });
110
+ }
111
+
112
+ // Custom libraries
113
+ customLibraries.forEach(({ url, globalName }) => {
114
+ libraryNames.push(globalName);
115
+ libraryPromises.push(this.loadScript(url, globalName));
116
+ });
117
+
118
+ // Load all libraries
119
+ const loadedLibraries = await Promise.all(libraryPromises);
120
+
121
+ // Build libraries object
122
+ const libraries: StandardLibraries = {
123
+ _: undefined // Initialize with required property
124
+ };
125
+ libraryNames.forEach((name, index) => {
126
+ // Map common names
127
+ if (name === '_') {
128
+ libraries._ = loadedLibraries[index];
129
+ } else {
130
+ libraries[name] = loadedLibraries[index];
131
+ }
132
+ });
133
+
134
+ // Ensure all standard properties exist
135
+ if (!libraries._) libraries._ = (window as any)._;
136
+ if (!libraries.d3) libraries.d3 = (window as any).d3;
137
+ if (!libraries.Chart) libraries.Chart = (window as any).Chart;
138
+ if (!libraries.dayjs) libraries.dayjs = (window as any).dayjs;
139
+ if (!libraries.antd) libraries.antd = (window as any).antd;
140
+ if (!libraries.ReactBootstrap) libraries.ReactBootstrap = (window as any).ReactBootstrap;
141
+
142
+ return {
143
+ React,
144
+ ReactDOM,
145
+ Babel,
146
+ libraries
147
+ };
148
+ }
149
+
150
+ /**
151
+ * Load a script from URL
152
+ */
153
+ private static async loadScript(url: string, globalName: string): Promise<any> {
154
+ // Check if already loaded
155
+ const existing = this.loadedResources.get(url);
156
+ if (existing) {
157
+ return existing.promise;
158
+ }
159
+
160
+ const promise = new Promise((resolve, reject) => {
161
+ // Check if global already exists
162
+ const existingGlobal = (window as any)[globalName];
163
+ if (existingGlobal) {
164
+ resolve(existingGlobal);
165
+ return;
166
+ }
167
+
168
+ // Check if script tag exists
169
+ const existingScript = document.querySelector(`script[src="${url}"]`);
170
+ if (existingScript) {
171
+ this.waitForScriptLoad(existingScript as HTMLScriptElement, globalName, resolve, reject);
172
+ return;
173
+ }
174
+
175
+ // Create new script
176
+ const script = document.createElement('script');
177
+ script.src = url;
178
+ script.async = true;
179
+ script.crossOrigin = 'anonymous';
180
+
181
+ script.onload = () => {
182
+ const global = (window as any)[globalName];
183
+ if (global) {
184
+ resolve(global);
185
+ } else {
186
+ // Some libraries may take a moment to initialize
187
+ setTimeout(() => {
188
+ const delayedGlobal = (window as any)[globalName];
189
+ if (delayedGlobal) {
190
+ resolve(delayedGlobal);
191
+ } else {
192
+ reject(new Error(`${globalName} not found after script load`));
193
+ }
194
+ }, 100);
195
+ }
196
+ };
197
+
198
+ script.onerror = () => {
199
+ reject(new Error(`Failed to load script: ${url}`));
200
+ };
201
+
202
+ document.head.appendChild(script);
203
+ });
204
+
205
+ this.loadedResources.set(url, {
206
+ element: document.querySelector(`script[src="${url}"]`)!,
207
+ promise
208
+ });
209
+
210
+ return promise;
211
+ }
212
+
213
+ /**
214
+ * Load CSS from URL
215
+ */
216
+ private static loadCSS(url: string): void {
217
+ if (this.loadedResources.has(url)) {
218
+ return;
219
+ }
220
+
221
+ const existingLink = document.querySelector(`link[href="${url}"]`);
222
+ if (existingLink) {
223
+ return;
224
+ }
225
+
226
+ const link = document.createElement('link');
227
+ link.rel = 'stylesheet';
228
+ link.href = url;
229
+ document.head.appendChild(link);
230
+
231
+ this.loadedResources.set(url, {
232
+ element: link,
233
+ promise: Promise.resolve()
234
+ });
235
+ }
236
+
237
+ /**
238
+ * Wait for existing script to load
239
+ */
240
+ private static waitForScriptLoad(
241
+ script: HTMLScriptElement,
242
+ globalName: string,
243
+ resolve: (value: any) => void,
244
+ reject: (reason: any) => void
245
+ ): void {
246
+ const checkGlobal = () => {
247
+ const global = (window as any)[globalName];
248
+ if (global) {
249
+ resolve(global);
250
+ } else {
251
+ // Retry after a short delay
252
+ setTimeout(() => {
253
+ const delayedGlobal = (window as any)[globalName];
254
+ if (delayedGlobal) {
255
+ resolve(delayedGlobal);
256
+ } else {
257
+ reject(new Error(`${globalName} not found after script load`));
258
+ }
259
+ }, 100);
260
+ }
261
+ };
262
+
263
+ // Check if already loaded
264
+ if ((script as any).complete || (script as any).readyState === 'complete') {
265
+ checkGlobal();
266
+ return;
267
+ }
268
+
269
+ // Wait for load
270
+ const loadHandler = () => {
271
+ script.removeEventListener('load', loadHandler);
272
+ checkGlobal();
273
+ };
274
+ script.addEventListener('load', loadHandler);
275
+ }
276
+
277
+ /**
278
+ * Get library name from URL for global variable mapping
279
+ */
280
+ private static getLibraryNameFromUrl(url: string): string {
281
+ // Map known URLs to their global variable names
282
+ if (url.includes('lodash')) return '_';
283
+ if (url.includes('d3')) return 'd3';
284
+ if (url.includes('Chart.js') || url.includes('chart')) return 'Chart';
285
+ if (url.includes('dayjs')) return 'dayjs';
286
+ if (url.includes('antd')) return 'antd';
287
+ if (url.includes('react-bootstrap')) return 'ReactBootstrap';
288
+
289
+ // Default: extract name from URL
290
+ const match = url.match(/\/([^\/]+)(?:\.min)?\.js$/);
291
+ return match ? match[1] : 'unknown';
292
+ }
293
+
294
+ /**
295
+ * Get all loaded resources (for cleanup)
296
+ */
297
+ static getLoadedResources(): Map<string, LoadedResource> {
298
+ return this.loadedResources;
299
+ }
300
+
301
+ /**
302
+ * Clear loaded resources cache
303
+ */
304
+ static clearCache(): void {
305
+ this.loadedResources.clear();
306
+ }
307
+ }