@memberjunction/react-runtime 2.71.0 → 2.73.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 +16 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +2 -1
- package/dist/runtime/prop-builder.d.ts +3 -1
- package/dist/runtime/prop-builder.d.ts.map +1 -1
- package/dist/runtime/prop-builder.js +82 -5
- 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 +6 -5
- package/src/index.ts +12 -0
- package/src/runtime/index.ts +1 -0
- package/src/runtime/prop-builder.ts +130 -4
- 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,193 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LibraryLoader = void 0;
|
|
4
|
+
const standard_libraries_1 = require("./standard-libraries");
|
|
5
|
+
class LibraryLoader {
|
|
6
|
+
static async loadAllLibraries() {
|
|
7
|
+
return this.loadLibraries({
|
|
8
|
+
loadCore: true,
|
|
9
|
+
loadUI: true,
|
|
10
|
+
loadCSS: true
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
static async loadLibraries(options) {
|
|
14
|
+
const { loadCore = true, loadUI = true, loadCSS = true, customLibraries = [] } = options;
|
|
15
|
+
const [React, ReactDOM, Babel] = await Promise.all([
|
|
16
|
+
this.loadScript(standard_libraries_1.STANDARD_LIBRARY_URLS.REACT, 'React'),
|
|
17
|
+
this.loadScript(standard_libraries_1.STANDARD_LIBRARY_URLS.REACT_DOM, 'ReactDOM'),
|
|
18
|
+
this.loadScript(standard_libraries_1.STANDARD_LIBRARY_URLS.BABEL, 'Babel')
|
|
19
|
+
]);
|
|
20
|
+
if (loadCSS) {
|
|
21
|
+
(0, standard_libraries_1.getCSSUrls)().forEach(url => this.loadCSS(url));
|
|
22
|
+
}
|
|
23
|
+
const libraryPromises = [];
|
|
24
|
+
const libraryNames = [];
|
|
25
|
+
if (loadCore) {
|
|
26
|
+
const coreUrls = (0, standard_libraries_1.getCoreLibraryUrls)();
|
|
27
|
+
coreUrls.forEach(url => {
|
|
28
|
+
const name = this.getLibraryNameFromUrl(url);
|
|
29
|
+
libraryNames.push(name);
|
|
30
|
+
libraryPromises.push(this.loadScript(url, name));
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
if (loadUI) {
|
|
34
|
+
const uiUrls = (0, standard_libraries_1.getUILibraryUrls)();
|
|
35
|
+
uiUrls.forEach(url => {
|
|
36
|
+
const name = this.getLibraryNameFromUrl(url);
|
|
37
|
+
libraryNames.push(name);
|
|
38
|
+
libraryPromises.push(this.loadScript(url, name));
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
customLibraries.forEach(({ url, globalName }) => {
|
|
42
|
+
libraryNames.push(globalName);
|
|
43
|
+
libraryPromises.push(this.loadScript(url, globalName));
|
|
44
|
+
});
|
|
45
|
+
const loadedLibraries = await Promise.all(libraryPromises);
|
|
46
|
+
const libraries = {
|
|
47
|
+
_: undefined
|
|
48
|
+
};
|
|
49
|
+
libraryNames.forEach((name, index) => {
|
|
50
|
+
if (name === '_') {
|
|
51
|
+
libraries._ = loadedLibraries[index];
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
libraries[name] = loadedLibraries[index];
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
if (!libraries._)
|
|
58
|
+
libraries._ = window._;
|
|
59
|
+
if (!libraries.d3)
|
|
60
|
+
libraries.d3 = window.d3;
|
|
61
|
+
if (!libraries.Chart)
|
|
62
|
+
libraries.Chart = window.Chart;
|
|
63
|
+
if (!libraries.dayjs)
|
|
64
|
+
libraries.dayjs = window.dayjs;
|
|
65
|
+
if (!libraries.antd)
|
|
66
|
+
libraries.antd = window.antd;
|
|
67
|
+
if (!libraries.ReactBootstrap)
|
|
68
|
+
libraries.ReactBootstrap = window.ReactBootstrap;
|
|
69
|
+
return {
|
|
70
|
+
React,
|
|
71
|
+
ReactDOM,
|
|
72
|
+
Babel,
|
|
73
|
+
libraries
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
static async loadScript(url, globalName) {
|
|
77
|
+
const existing = this.loadedResources.get(url);
|
|
78
|
+
if (existing) {
|
|
79
|
+
return existing.promise;
|
|
80
|
+
}
|
|
81
|
+
const promise = new Promise((resolve, reject) => {
|
|
82
|
+
const existingGlobal = window[globalName];
|
|
83
|
+
if (existingGlobal) {
|
|
84
|
+
resolve(existingGlobal);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const existingScript = document.querySelector(`script[src="${url}"]`);
|
|
88
|
+
if (existingScript) {
|
|
89
|
+
this.waitForScriptLoad(existingScript, globalName, resolve, reject);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const script = document.createElement('script');
|
|
93
|
+
script.src = url;
|
|
94
|
+
script.async = true;
|
|
95
|
+
script.crossOrigin = 'anonymous';
|
|
96
|
+
script.onload = () => {
|
|
97
|
+
const global = window[globalName];
|
|
98
|
+
if (global) {
|
|
99
|
+
resolve(global);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
setTimeout(() => {
|
|
103
|
+
const delayedGlobal = window[globalName];
|
|
104
|
+
if (delayedGlobal) {
|
|
105
|
+
resolve(delayedGlobal);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
reject(new Error(`${globalName} not found after script load`));
|
|
109
|
+
}
|
|
110
|
+
}, 100);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
script.onerror = () => {
|
|
114
|
+
reject(new Error(`Failed to load script: ${url}`));
|
|
115
|
+
};
|
|
116
|
+
document.head.appendChild(script);
|
|
117
|
+
});
|
|
118
|
+
this.loadedResources.set(url, {
|
|
119
|
+
element: document.querySelector(`script[src="${url}"]`),
|
|
120
|
+
promise
|
|
121
|
+
});
|
|
122
|
+
return promise;
|
|
123
|
+
}
|
|
124
|
+
static loadCSS(url) {
|
|
125
|
+
if (this.loadedResources.has(url)) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const existingLink = document.querySelector(`link[href="${url}"]`);
|
|
129
|
+
if (existingLink) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const link = document.createElement('link');
|
|
133
|
+
link.rel = 'stylesheet';
|
|
134
|
+
link.href = url;
|
|
135
|
+
document.head.appendChild(link);
|
|
136
|
+
this.loadedResources.set(url, {
|
|
137
|
+
element: link,
|
|
138
|
+
promise: Promise.resolve()
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
static waitForScriptLoad(script, globalName, resolve, reject) {
|
|
142
|
+
const checkGlobal = () => {
|
|
143
|
+
const global = window[globalName];
|
|
144
|
+
if (global) {
|
|
145
|
+
resolve(global);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
setTimeout(() => {
|
|
149
|
+
const delayedGlobal = window[globalName];
|
|
150
|
+
if (delayedGlobal) {
|
|
151
|
+
resolve(delayedGlobal);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
reject(new Error(`${globalName} not found after script load`));
|
|
155
|
+
}
|
|
156
|
+
}, 100);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
if (script.complete || script.readyState === 'complete') {
|
|
160
|
+
checkGlobal();
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const loadHandler = () => {
|
|
164
|
+
script.removeEventListener('load', loadHandler);
|
|
165
|
+
checkGlobal();
|
|
166
|
+
};
|
|
167
|
+
script.addEventListener('load', loadHandler);
|
|
168
|
+
}
|
|
169
|
+
static getLibraryNameFromUrl(url) {
|
|
170
|
+
if (url.includes('lodash'))
|
|
171
|
+
return '_';
|
|
172
|
+
if (url.includes('d3'))
|
|
173
|
+
return 'd3';
|
|
174
|
+
if (url.includes('Chart.js') || url.includes('chart'))
|
|
175
|
+
return 'Chart';
|
|
176
|
+
if (url.includes('dayjs'))
|
|
177
|
+
return 'dayjs';
|
|
178
|
+
if (url.includes('antd'))
|
|
179
|
+
return 'antd';
|
|
180
|
+
if (url.includes('react-bootstrap'))
|
|
181
|
+
return 'ReactBootstrap';
|
|
182
|
+
const match = url.match(/\/([^\/]+)(?:\.min)?\.js$/);
|
|
183
|
+
return match ? match[1] : 'unknown';
|
|
184
|
+
}
|
|
185
|
+
static getLoadedResources() {
|
|
186
|
+
return this.loadedResources;
|
|
187
|
+
}
|
|
188
|
+
static clearCache() {
|
|
189
|
+
this.loadedResources.clear();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
exports.LibraryLoader = LibraryLoader;
|
|
193
|
+
LibraryLoader.loadedResources = new Map();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memberjunction/react-runtime",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.73.0",
|
|
4
4
|
"description": "Platform-agnostic React component runtime for MemberJunction. Provides core compilation, registry, and execution capabilities for React components in any JavaScript environment.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -25,10 +25,11 @@
|
|
|
25
25
|
},
|
|
26
26
|
"homepage": "https://github.com/MemberJunction/MJ#readme",
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@memberjunction/core": "2.
|
|
29
|
-
"@memberjunction/global": "2.
|
|
30
|
-
"@memberjunction/interactive-component-types": "2.
|
|
31
|
-
"@babel/standalone": "^7.23.5"
|
|
28
|
+
"@memberjunction/core": "2.73.0",
|
|
29
|
+
"@memberjunction/global": "2.73.0",
|
|
30
|
+
"@memberjunction/interactive-component-types": "2.73.0",
|
|
31
|
+
"@babel/standalone": "^7.23.5",
|
|
32
|
+
"rxjs": "^7.8.1"
|
|
32
33
|
},
|
|
33
34
|
"devDependencies": {
|
|
34
35
|
"@types/node": "20.10.0",
|
package/src/index.ts
CHANGED
|
@@ -53,6 +53,7 @@ export {
|
|
|
53
53
|
|
|
54
54
|
export {
|
|
55
55
|
buildComponentProps,
|
|
56
|
+
cleanupPropBuilder,
|
|
56
57
|
normalizeCallbacks,
|
|
57
58
|
normalizeStyles,
|
|
58
59
|
validateComponentProps,
|
|
@@ -94,6 +95,17 @@ export {
|
|
|
94
95
|
createStandardLibraries
|
|
95
96
|
} from './utilities/standard-libraries';
|
|
96
97
|
|
|
98
|
+
export {
|
|
99
|
+
LibraryLoader,
|
|
100
|
+
LibraryLoadOptions,
|
|
101
|
+
LibraryLoadResult
|
|
102
|
+
} from './utilities/library-loader';
|
|
103
|
+
|
|
104
|
+
export {
|
|
105
|
+
ComponentErrorAnalyzer,
|
|
106
|
+
FailedComponentInfo
|
|
107
|
+
} from './utilities/component-error-analyzer';
|
|
108
|
+
|
|
97
109
|
// Version information
|
|
98
110
|
export const VERSION = '2.69.1';
|
|
99
111
|
|
package/src/runtime/index.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { ComponentProps, ComponentCallbacks, ComponentStyles } from '../types';
|
|
8
|
+
import { Subject, debounceTime, Subscription } from 'rxjs';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Options for building component props
|
|
@@ -18,6 +19,8 @@ export interface PropBuilderOptions {
|
|
|
18
19
|
transformData?: (data: any) => any;
|
|
19
20
|
/** Transform state before passing to component */
|
|
20
21
|
transformState?: (state: any) => any;
|
|
22
|
+
/** Debounce time for UpdateUserState callback in milliseconds */
|
|
23
|
+
debounceUpdateUserState?: number;
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
/**
|
|
@@ -43,7 +46,8 @@ export function buildComponentProps(
|
|
|
43
46
|
const {
|
|
44
47
|
validate = true,
|
|
45
48
|
transformData,
|
|
46
|
-
transformState
|
|
49
|
+
transformState,
|
|
50
|
+
debounceUpdateUserState = 3000 // Default 3 seconds
|
|
47
51
|
} = options;
|
|
48
52
|
|
|
49
53
|
// Transform data if transformer provided
|
|
@@ -55,7 +59,7 @@ export function buildComponentProps(
|
|
|
55
59
|
data: transformedData,
|
|
56
60
|
userState: transformedState,
|
|
57
61
|
utilities,
|
|
58
|
-
callbacks: normalizeCallbacks(callbacks),
|
|
62
|
+
callbacks: normalizeCallbacks(callbacks, debounceUpdateUserState),
|
|
59
63
|
components,
|
|
60
64
|
styles: normalizeStyles(styles)
|
|
61
65
|
};
|
|
@@ -68,12 +72,47 @@ export function buildComponentProps(
|
|
|
68
72
|
return props;
|
|
69
73
|
}
|
|
70
74
|
|
|
75
|
+
// Store subjects for debouncing per component instance
|
|
76
|
+
const updateUserStateSubjects = new WeakMap<Function, Subject<any>>();
|
|
77
|
+
|
|
78
|
+
// Store subscriptions for cleanup
|
|
79
|
+
const updateUserStateSubscriptions = new WeakMap<Function, Subscription>();
|
|
80
|
+
|
|
81
|
+
// Loop detection state
|
|
82
|
+
interface LoopDetectionState {
|
|
83
|
+
count: number;
|
|
84
|
+
lastUpdate: number;
|
|
85
|
+
lastState: any;
|
|
86
|
+
}
|
|
87
|
+
const loopDetectionStates = new WeakMap<Function, LoopDetectionState>();
|
|
88
|
+
|
|
89
|
+
// Deep equality check for objects
|
|
90
|
+
function deepEqual(obj1: any, obj2: any): boolean {
|
|
91
|
+
if (obj1 === obj2) return true;
|
|
92
|
+
|
|
93
|
+
if (!obj1 || !obj2) return false;
|
|
94
|
+
if (typeof obj1 !== 'object' || typeof obj2 !== 'object') return false;
|
|
95
|
+
|
|
96
|
+
const keys1 = Object.keys(obj1);
|
|
97
|
+
const keys2 = Object.keys(obj2);
|
|
98
|
+
|
|
99
|
+
if (keys1.length !== keys2.length) return false;
|
|
100
|
+
|
|
101
|
+
for (const key of keys1) {
|
|
102
|
+
if (!keys2.includes(key)) return false;
|
|
103
|
+
if (!deepEqual(obj1[key], obj2[key])) return false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
71
109
|
/**
|
|
72
110
|
* Normalizes component callbacks
|
|
73
111
|
* @param callbacks - Raw callbacks object
|
|
112
|
+
* @param debounceMs - Debounce time for UpdateUserState in milliseconds
|
|
74
113
|
* @returns Normalized callbacks
|
|
75
114
|
*/
|
|
76
|
-
export function normalizeCallbacks(callbacks: any): ComponentCallbacks {
|
|
115
|
+
export function normalizeCallbacks(callbacks: any, debounceMs: number = 3000): ComponentCallbacks {
|
|
77
116
|
const normalized: ComponentCallbacks = {};
|
|
78
117
|
|
|
79
118
|
// Ensure all callbacks are functions
|
|
@@ -86,7 +125,67 @@ export function normalizeCallbacks(callbacks: any): ComponentCallbacks {
|
|
|
86
125
|
}
|
|
87
126
|
|
|
88
127
|
if (callbacks.UpdateUserState && typeof callbacks.UpdateUserState === 'function') {
|
|
89
|
-
|
|
128
|
+
// Create a debounced version of UpdateUserState with loop detection
|
|
129
|
+
const originalCallback = callbacks.UpdateUserState;
|
|
130
|
+
|
|
131
|
+
// Get or create a subject for this callback
|
|
132
|
+
let subject = updateUserStateSubjects.get(originalCallback);
|
|
133
|
+
if (!subject) {
|
|
134
|
+
subject = new Subject<any>();
|
|
135
|
+
updateUserStateSubjects.set(originalCallback, subject);
|
|
136
|
+
|
|
137
|
+
// Subscribe to the subject with debounce
|
|
138
|
+
const subscription = subject.pipe(
|
|
139
|
+
debounceTime(debounceMs)
|
|
140
|
+
).subscribe(state => {
|
|
141
|
+
console.log(`[Skip Component] UpdateUserState called after ${debounceMs}ms debounce`);
|
|
142
|
+
originalCallback(state);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Store the subscription for cleanup
|
|
146
|
+
updateUserStateSubscriptions.set(originalCallback, subscription);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Get or create loop detection state
|
|
150
|
+
let loopState = loopDetectionStates.get(originalCallback);
|
|
151
|
+
if (!loopState) {
|
|
152
|
+
loopState = { count: 0, lastUpdate: 0, lastState: null };
|
|
153
|
+
loopDetectionStates.set(originalCallback, loopState);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Return a function that prevents redundant updates
|
|
157
|
+
normalized.UpdateUserState = (state: any) => {
|
|
158
|
+
// Check if this is a redundant update
|
|
159
|
+
if (loopState!.lastState && deepEqual(state, loopState!.lastState)) {
|
|
160
|
+
console.log('[Skip Component] Skipping redundant state update');
|
|
161
|
+
return; // Don't process identical state updates
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const now = Date.now();
|
|
165
|
+
const timeSinceLastUpdate = now - loopState!.lastUpdate;
|
|
166
|
+
|
|
167
|
+
// Check for rapid updates
|
|
168
|
+
if (timeSinceLastUpdate < 100) {
|
|
169
|
+
loopState!.count++;
|
|
170
|
+
|
|
171
|
+
if (loopState!.count > 5) {
|
|
172
|
+
console.error('[Skip Component] Rapid state updates detected - possible infinite loop');
|
|
173
|
+
console.error('Updates in last 100ms:', loopState!.count);
|
|
174
|
+
// Still process the update but warn
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
// Reset counter if more than 100ms has passed
|
|
178
|
+
loopState!.count = 0;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
loopState!.lastUpdate = now;
|
|
182
|
+
loopState!.lastState = JSON.parse(JSON.stringify(state)); // Deep clone to preserve state
|
|
183
|
+
|
|
184
|
+
console.log('[Skip Component] Processing state update');
|
|
185
|
+
|
|
186
|
+
// Push to debounce subject (which already has 3 second debounce)
|
|
187
|
+
subject!.next(state);
|
|
188
|
+
};
|
|
90
189
|
}
|
|
91
190
|
|
|
92
191
|
if (callbacks.NotifyEvent && typeof callbacks.NotifyEvent === 'function') {
|
|
@@ -186,6 +285,33 @@ export function mergeProps(...propsList: Partial<ComponentProps>[]): ComponentPr
|
|
|
186
285
|
return merged;
|
|
187
286
|
}
|
|
188
287
|
|
|
288
|
+
/**
|
|
289
|
+
* Cleanup function for prop builder resources
|
|
290
|
+
* @param callbacks - The callbacks object that was used to build props
|
|
291
|
+
*/
|
|
292
|
+
export function cleanupPropBuilder(callbacks: ComponentCallbacks): void {
|
|
293
|
+
if (callbacks.UpdateUserState && typeof callbacks.UpdateUserState === 'function') {
|
|
294
|
+
const originalCallback = callbacks.UpdateUserState;
|
|
295
|
+
|
|
296
|
+
// Unsubscribe from the subject
|
|
297
|
+
const subscription = updateUserStateSubscriptions.get(originalCallback);
|
|
298
|
+
if (subscription) {
|
|
299
|
+
subscription.unsubscribe();
|
|
300
|
+
updateUserStateSubscriptions.delete(originalCallback);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Complete and remove the subject
|
|
304
|
+
const subject = updateUserStateSubjects.get(originalCallback);
|
|
305
|
+
if (subject) {
|
|
306
|
+
subject.complete();
|
|
307
|
+
updateUserStateSubjects.delete(originalCallback);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Clear loop detection state
|
|
311
|
+
loopDetectionStates.delete(originalCallback);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
189
315
|
/**
|
|
190
316
|
* Creates a props transformer function
|
|
191
317
|
* @param transformations - Map of prop paths to transformer functions
|