@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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +8 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/utilities/component-error-analyzer.d.ts +20 -0
- package/dist/utilities/component-error-analyzer.d.ts.map +1 -0
- package/dist/utilities/component-error-analyzer.js +204 -0
- package/dist/utilities/index.d.ts +2 -0
- package/dist/utilities/index.d.ts.map +1 -1
- package/dist/utilities/index.js +2 -0
- package/dist/utilities/library-loader.d.ts +33 -0
- package/dist/utilities/library-loader.d.ts.map +1 -0
- package/dist/utilities/library-loader.js +193 -0
- package/package.json +4 -4
- package/src/index.ts +11 -0
- package/src/utilities/component-error-analyzer.ts +315 -0
- package/src/utilities/index.ts +3 -1
- package/src/utilities/library-loader.ts +307 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -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
|
+
}
|
package/src/utilities/index.ts
CHANGED
|
@@ -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
|
+
}
|