@memberjunction/react-runtime 4.1.0 → 4.3.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/package.json CHANGED
@@ -1,17 +1,19 @@
1
1
  {
2
2
  "name": "@memberjunction/react-runtime",
3
- "version": "4.1.0",
3
+ "version": "4.3.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
+ "type": "module",
5
6
  "main": "dist/index.js",
6
7
  "types": "dist/index.d.ts",
7
8
  "scripts": {
8
9
  "build": "npm run build:node && npm run build:umd",
9
10
  "build:node": "tsc",
10
- "build:umd": "webpack --config webpack.umd.config.js",
11
+ "build:umd": "webpack --config webpack.umd.config.cjs",
11
12
  "build:clean": "rimraf ./dist && npm run build",
12
13
  "watch": "tsc -w",
13
14
  "patchVersion": "npm version patch",
14
- "test": "jest"
15
+ "test": "vitest run",
16
+ "test:watch": "vitest"
15
17
  },
16
18
  "keywords": [
17
19
  "memberjunction",
@@ -27,18 +29,18 @@
27
29
  },
28
30
  "homepage": "https://github.com/MemberJunction/MJ#readme",
29
31
  "dependencies": {
30
- "@memberjunction/core": "4.1.0",
31
- "@memberjunction/global": "4.1.0",
32
- "@memberjunction/interactive-component-types": "4.1.0",
33
- "@memberjunction/core-entities": "4.1.0",
34
- "@memberjunction/graphql-dataprovider": "4.1.0",
32
+ "@memberjunction/core": "4.3.0",
33
+ "@memberjunction/global": "4.3.0",
34
+ "@memberjunction/interactive-component-types": "4.3.0",
35
+ "@memberjunction/core-entities": "4.3.0",
36
+ "@memberjunction/graphql-dataprovider": "4.3.0",
35
37
  "@babel/standalone": "^7.29.1",
36
38
  "rxjs": "^7.8.2"
37
39
  },
38
40
  "devDependencies": {
39
41
  "@types/node": "24.10.11",
40
- "jest": "^30.2.0",
41
42
  "rimraf": "^6.1.2",
43
+ "vitest": "^4.0.18",
42
44
  "typescript": "~5.9.3",
43
45
  "webpack": "^5.105.0",
44
46
  "webpack-cli": "^6.0.1",
@@ -0,0 +1,162 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+
3
+ // Mock window for timer functions
4
+ const mockSetInterval = vi.fn().mockReturnValue(1);
5
+ const mockClearInterval = vi.fn();
6
+ vi.stubGlobal('window', {
7
+ setInterval: mockSetInterval,
8
+ clearInterval: mockClearInterval,
9
+ });
10
+
11
+ import { CacheManager } from '../utilities/cache-manager';
12
+
13
+ describe('CacheManager', () => {
14
+ let cache: CacheManager<string>;
15
+
16
+ beforeEach(() => {
17
+ vi.useFakeTimers();
18
+ mockSetInterval.mockClear();
19
+ mockClearInterval.mockClear();
20
+ cache = new CacheManager<string>({
21
+ maxSize: 5,
22
+ defaultTTL: 10000, // 10 seconds
23
+ cleanupInterval: 0, // disable auto cleanup for most tests
24
+ });
25
+ });
26
+
27
+ afterEach(() => {
28
+ cache.destroy();
29
+ vi.useRealTimers();
30
+ });
31
+
32
+ describe('set and get', () => {
33
+ it('should store and retrieve a value', () => {
34
+ cache.set('key1', 'value1');
35
+ expect(cache.get('key1')).toBe('value1');
36
+ });
37
+
38
+ it('should return undefined for missing key', () => {
39
+ expect(cache.get('nonexistent')).toBeUndefined();
40
+ });
41
+
42
+ it('should overwrite existing value', () => {
43
+ cache.set('key1', 'value1');
44
+ cache.set('key1', 'value2');
45
+ expect(cache.get('key1')).toBe('value2');
46
+ });
47
+ });
48
+
49
+ describe('has', () => {
50
+ it('should return true for existing key', () => {
51
+ cache.set('key1', 'value1');
52
+ expect(cache.has('key1')).toBe(true);
53
+ });
54
+
55
+ it('should return false for missing key', () => {
56
+ expect(cache.has('nonexistent')).toBe(false);
57
+ });
58
+ });
59
+
60
+ describe('delete', () => {
61
+ it('should remove a key', () => {
62
+ cache.set('key1', 'value1');
63
+ expect(cache.delete('key1')).toBe(true);
64
+ expect(cache.get('key1')).toBeUndefined();
65
+ });
66
+
67
+ it('should return false for non-existent key', () => {
68
+ expect(cache.delete('nonexistent')).toBe(false);
69
+ });
70
+ });
71
+
72
+ describe('clear', () => {
73
+ it('should remove all entries', () => {
74
+ cache.set('key1', 'value1');
75
+ cache.set('key2', 'value2');
76
+ cache.clear();
77
+ expect(cache.get('key1')).toBeUndefined();
78
+ expect(cache.get('key2')).toBeUndefined();
79
+ });
80
+
81
+ it('should reset memory usage', () => {
82
+ cache.set('key1', 'value1');
83
+ cache.clear();
84
+ const stats = cache.getStats();
85
+ expect(stats.size).toBe(0);
86
+ expect(stats.memoryUsage).toBe(0);
87
+ });
88
+ });
89
+
90
+ describe('getStats', () => {
91
+ it('should return cache statistics', () => {
92
+ cache.set('key1', 'value1');
93
+ const stats = cache.getStats();
94
+ expect(stats.size).toBe(1);
95
+ expect(stats.maxSize).toBe(5);
96
+ expect(stats.memoryUsage).toBeGreaterThan(0);
97
+ });
98
+ });
99
+
100
+ describe('TTL expiration', () => {
101
+ it('should return undefined for expired entries on get', () => {
102
+ cache.set('key1', 'value1');
103
+ vi.advanceTimersByTime(11000); // advance past 10s TTL
104
+ expect(cache.get('key1')).toBeUndefined();
105
+ });
106
+
107
+ it('should report false for expired entries on has', () => {
108
+ cache.set('key1', 'value1');
109
+ vi.advanceTimersByTime(11000);
110
+ expect(cache.has('key1')).toBe(false);
111
+ });
112
+ });
113
+
114
+ describe('LRU eviction', () => {
115
+ it('should evict oldest entry when max size reached', () => {
116
+ // Fill to capacity (maxSize = 5)
117
+ for (let i = 0; i < 5; i++) {
118
+ cache.set(`key${i}`, `value${i}`);
119
+ vi.advanceTimersByTime(100); // separate timestamps
120
+ }
121
+
122
+ // Adding one more should evict the oldest (key0)
123
+ cache.set('key5', 'value5');
124
+
125
+ // key0 should be evicted
126
+ expect(cache.has('key0')).toBe(false);
127
+ // newest should be present
128
+ expect(cache.get('key5')).toBe('value5');
129
+ });
130
+ });
131
+
132
+ describe('cleanup', () => {
133
+ it('should remove expired entries that scheduled timeout has not yet removed', () => {
134
+ // When set() is called, it schedules a setTimeout for TTL deletion.
135
+ // With fake timers, advancing time triggers those scheduled removals.
136
+ // After the TTL, entries are already removed by their scheduled timeouts.
137
+ cache.set('key1', 'value1');
138
+ cache.set('key2', 'value2');
139
+ expect(cache.getStats().size).toBe(2);
140
+ // Advance time past TTL -- the scheduled timeouts will fire and remove entries
141
+ vi.advanceTimersByTime(11000);
142
+ // Entries already removed by scheduled timeout, so cleanup finds nothing
143
+ const removed = cache.cleanup();
144
+ expect(removed).toBe(0);
145
+ expect(cache.getStats().size).toBe(0);
146
+ });
147
+
148
+ it('should return 0 when nothing to clean', () => {
149
+ cache.set('key1', 'value1');
150
+ const removed = cache.cleanup();
151
+ expect(removed).toBe(0);
152
+ });
153
+ });
154
+
155
+ describe('destroy', () => {
156
+ it('should clear the cache and stop timers', () => {
157
+ cache.set('key1', 'value1');
158
+ cache.destroy();
159
+ expect(cache.getStats().size).toBe(0);
160
+ });
161
+ });
162
+ });
@@ -0,0 +1,116 @@
1
+ import { describe, it, expect } from 'vitest';
2
+
3
+ import { ComponentErrorAnalyzer } from '../utilities/component-error-analyzer';
4
+
5
+ describe('ComponentErrorAnalyzer', () => {
6
+ describe('identifyFailedComponents', () => {
7
+ it('should return empty array for empty errors', () => {
8
+ expect(ComponentErrorAnalyzer.identifyFailedComponents([])).toEqual([]);
9
+ });
10
+
11
+ it('should identify component from ReferenceError', () => {
12
+ const errors = ['ReferenceError: MyComponent is not defined'];
13
+ const result = ComponentErrorAnalyzer.identifyFailedComponents(errors);
14
+ expect(result).toContain('MyComponent');
15
+ });
16
+
17
+ it('should identify component from render error', () => {
18
+ const errors = ['MyWidget(...): Nothing was returned from render'];
19
+ const result = ComponentErrorAnalyzer.identifyFailedComponents(errors);
20
+ expect(result).toContain('MyWidget');
21
+ });
22
+
23
+ it('should identify component from not-a-function error', () => {
24
+ const errors = ['SomeComponent is not a function'];
25
+ const result = ComponentErrorAnalyzer.identifyFailedComponents(errors);
26
+ expect(result).toContain('SomeComponent');
27
+ });
28
+
29
+ it('should not identify JavaScript builtins as components', () => {
30
+ const errors = ['ReferenceError: Object is not defined'];
31
+ const result = ComponentErrorAnalyzer.identifyFailedComponents(errors);
32
+ expect(result).not.toContain('Object');
33
+ });
34
+
35
+ it('should not identify hooks as components', () => {
36
+ const errors = ['ReferenceError: useState is not defined'];
37
+ const result = ComponentErrorAnalyzer.identifyFailedComponents(errors);
38
+ expect(result).not.toContain('useState');
39
+ });
40
+
41
+ it('should deduplicate component names', () => {
42
+ const errors = [
43
+ 'ReferenceError: MyComponent is not defined',
44
+ 'MyComponent is not a function',
45
+ ];
46
+ const result = ComponentErrorAnalyzer.identifyFailedComponents(errors);
47
+ const uniqueCount = result.filter(c => c === 'MyComponent').length;
48
+ expect(uniqueCount).toBe(1);
49
+ });
50
+ });
51
+
52
+ describe('analyzeComponentErrors', () => {
53
+ it('should return empty array for no errors', () => {
54
+ expect(ComponentErrorAnalyzer.analyzeComponentErrors([])).toEqual([]);
55
+ });
56
+
57
+ it('should return detailed failure info for ReferenceError', () => {
58
+ const errors = ['ReferenceError: DashboardPanel is not defined'];
59
+ const failures = ComponentErrorAnalyzer.analyzeComponentErrors(errors);
60
+ expect(failures.length).toBeGreaterThan(0);
61
+ const panelFailure = failures.find(f => f.componentName === 'DashboardPanel');
62
+ expect(panelFailure).toBeDefined();
63
+ expect(panelFailure!.errorType).toBe('not_defined');
64
+ });
65
+
66
+ it('should extract line numbers from errors', () => {
67
+ const errors = ['ReferenceError: MyComp is not defined at main.js:42:10'];
68
+ const failures = ComponentErrorAnalyzer.analyzeComponentErrors(errors);
69
+ const compFailure = failures.find(f => f.componentName === 'MyComp');
70
+ expect(compFailure).toBeDefined();
71
+ expect(compFailure!.lineNumber).toBe(42);
72
+ });
73
+
74
+ it('should deduplicate failures by component name and error type', () => {
75
+ const errors = [
76
+ 'ReferenceError: Widget is not defined',
77
+ 'ReferenceError: Widget is not defined at line 42',
78
+ ];
79
+ const failures = ComponentErrorAnalyzer.analyzeComponentErrors(errors);
80
+ const widgetFailures = failures.filter(f => f.componentName === 'Widget' && f.errorType === 'not_defined');
81
+ expect(widgetFailures.length).toBe(1);
82
+ });
83
+ });
84
+
85
+ describe('formatAnalysisResults', () => {
86
+ it('should format empty results message', () => {
87
+ const result = ComponentErrorAnalyzer.formatAnalysisResults([]);
88
+ expect(result).toBe('No component failures detected');
89
+ });
90
+
91
+ it('should format failure details', () => {
92
+ const failures = [{
93
+ componentName: 'TestComponent',
94
+ errorType: 'not_defined',
95
+ errorMessage: 'ReferenceError: TestComponent is not defined',
96
+ lineNumber: 10,
97
+ }];
98
+ const result = ComponentErrorAnalyzer.formatAnalysisResults(failures);
99
+ expect(result).toContain('1 component failure(s)');
100
+ expect(result).toContain('TestComponent');
101
+ expect(result).toContain('not_defined');
102
+ expect(result).toContain('Line: 10');
103
+ });
104
+
105
+ it('should truncate long error messages', () => {
106
+ const longMessage = 'A'.repeat(300);
107
+ const failures = [{
108
+ componentName: 'BigError',
109
+ errorType: 'unknown',
110
+ errorMessage: longMessage,
111
+ }];
112
+ const result = ComponentErrorAnalyzer.formatAnalysisResults(failures);
113
+ expect(result).toContain('...');
114
+ });
115
+ });
116
+ });
package/tsconfig.json CHANGED
@@ -20,5 +20,5 @@
20
20
  "types": ["node"]
21
21
  },
22
22
  "include": ["src/**/*"],
23
- "exclude": ["node_modules", "dist"]
23
+ "exclude": ["node_modules", "dist", "src/__tests__/**", "src/**/*.test.ts", "src/**/*.spec.ts"]
24
24
  }
@@ -0,0 +1,8 @@
1
+ import { defineProject, mergeConfig } from 'vitest/config';
2
+ import sharedConfig from '../../../vitest.shared';
3
+
4
+ export default mergeConfig(sharedConfig, defineProject({
5
+ test: {
6
+ environment: 'node',
7
+ },
8
+ }));
@@ -40,6 +40,10 @@ module.exports = {
40
40
  }]
41
41
  ]
42
42
  }
43
+ },
44
+ // Disable ESM's strict extension requirement for local imports
45
+ resolve: {
46
+ fullySpecified: false
43
47
  }
44
48
  }
45
49
  ]