@memberjunction/react-runtime 5.32.0 → 5.34.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,6 +1,6 @@
1
1
  {
2
2
  "name": "@memberjunction/react-runtime",
3
- "version": "5.32.0",
3
+ "version": "5.34.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
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -29,11 +29,11 @@
29
29
  },
30
30
  "homepage": "https://github.com/MemberJunction/MJ#readme",
31
31
  "dependencies": {
32
- "@memberjunction/core": "5.32.0",
33
- "@memberjunction/global": "5.32.0",
34
- "@memberjunction/interactive-component-types": "5.32.0",
35
- "@memberjunction/core-entities": "5.32.0",
36
- "@memberjunction/graphql-dataprovider": "5.32.0",
32
+ "@memberjunction/core": "5.34.0",
33
+ "@memberjunction/global": "5.34.0",
34
+ "@memberjunction/interactive-component-types": "5.34.0",
35
+ "@memberjunction/core-entities": "5.34.0",
36
+ "@memberjunction/graphql-dataprovider": "5.34.0",
37
37
  "@babel/standalone": "^7.29.1",
38
38
  "rxjs": "^7.8.2"
39
39
  },
@@ -0,0 +1,340 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+
3
+ // Stub globals before imports
4
+ vi.stubGlobal('window', {
5
+ setInterval: vi.fn().mockReturnValue(1),
6
+ clearInterval: vi.fn(),
7
+ });
8
+
9
+ // Mock ComponentMetadataEngine before importing ComponentManager
10
+ vi.mock('@memberjunction/core-entities', () => ({
11
+ ComponentMetadataEngine: {
12
+ Instance: {
13
+ Config: vi.fn().mockResolvedValue(undefined),
14
+ Components: [],
15
+ ComponentLibraries: [],
16
+ },
17
+ },
18
+ MJComponentLibraryEntity: class {},
19
+ MJComponentEntityExtended: class {},
20
+ }));
21
+
22
+ vi.mock('@memberjunction/core', () => ({
23
+ UserInfo: class {},
24
+ Metadata: class {},
25
+ LogError: vi.fn(),
26
+ }));
27
+
28
+ import { ComponentManager } from '../component-manager/component-manager';
29
+ import { ComponentRegistry } from '../registry/component-registry';
30
+ import { ComponentSpec } from '@memberjunction/interactive-component-types';
31
+
32
+ /**
33
+ * Creates a minimal mock ComponentCompiler.
34
+ * The compile() result returns a factory that produces a tagged component object
35
+ * so tests can verify which code was actually compiled.
36
+ */
37
+ function createMockCompiler() {
38
+ return {
39
+ compile: vi.fn().mockImplementation(async (opts: { componentName: string; componentCode: string }) => ({
40
+ success: true,
41
+ component: {
42
+ factory: () => ({ __name: opts.componentName, __code: opts.componentCode }),
43
+ },
44
+ loadedLibraries: new Map(),
45
+ })),
46
+ setBabelInstance: vi.fn(),
47
+ } as any;
48
+ }
49
+
50
+ /**
51
+ * Creates a minimal mock RuntimeContext.
52
+ */
53
+ function createMockRuntimeContext() {
54
+ return {
55
+ React: {},
56
+ globals: {},
57
+ libraries: {},
58
+ getLibrary: vi.fn(),
59
+ } as any;
60
+ }
61
+
62
+ /**
63
+ * Helper to build a ComponentSpec with sensible defaults.
64
+ */
65
+ function makeSpec(overrides: Partial<ComponentSpec> & { name: string }): ComponentSpec {
66
+ return {
67
+ location: 'embedded',
68
+ version: '1.0.0',
69
+ namespace: 'Test',
70
+ code: `function ${overrides.name}() { return null; }`,
71
+ ...overrides,
72
+ } as ComponentSpec;
73
+ }
74
+
75
+ describe('ComponentManager', () => {
76
+ let compiler: ReturnType<typeof createMockCompiler>;
77
+ let registry: ComponentRegistry;
78
+ let manager: ComponentManager;
79
+
80
+ beforeEach(() => {
81
+ compiler = createMockCompiler();
82
+ registry = new ComponentRegistry({ cleanupInterval: 0 });
83
+ manager = new ComponentManager(compiler, registry, createMockRuntimeContext(), {
84
+ debug: false,
85
+ enableUsageTracking: false,
86
+ });
87
+ });
88
+
89
+ // --------------------------------------------------------------------------
90
+ // Basic loading
91
+ // --------------------------------------------------------------------------
92
+ describe('loadComponent', () => {
93
+ it('should compile and return a component on first load', async () => {
94
+ const spec = makeSpec({ name: 'Button' });
95
+ const result = await manager.loadComponent(spec);
96
+
97
+ expect(result.success).toBe(true);
98
+ expect(result.fromCache).toBe(false);
99
+ expect(compiler.compile).toHaveBeenCalledTimes(1);
100
+ });
101
+
102
+ it('should return from cache on second load with same spec', async () => {
103
+ const spec = makeSpec({ name: 'Button' });
104
+
105
+ await manager.loadComponent(spec);
106
+ compiler.compile.mockClear();
107
+
108
+ const result2 = await manager.loadComponent(spec);
109
+ expect(result2.success).toBe(true);
110
+ expect(result2.fromCache).toBe(true);
111
+ expect(compiler.compile).not.toHaveBeenCalled();
112
+ });
113
+
114
+ it('should recompile when code changes (different hash)', async () => {
115
+ const specV1 = makeSpec({ name: 'Button', code: 'function Button() { return 1; }' });
116
+ const specV2 = makeSpec({ name: 'Button', code: 'function Button() { return 2; }' });
117
+
118
+ await manager.loadComponent(specV1);
119
+ compiler.compile.mockClear();
120
+
121
+ const result = await manager.loadComponent(specV2);
122
+ expect(result.success).toBe(true);
123
+ expect(result.fromCache).toBe(false);
124
+ expect(compiler.compile).toHaveBeenCalledTimes(1);
125
+ });
126
+
127
+ it('should recompile when forceRecompile is set', async () => {
128
+ const spec = makeSpec({ name: 'Button' });
129
+
130
+ await manager.loadComponent(spec);
131
+ compiler.compile.mockClear();
132
+
133
+ const result = await manager.loadComponent(spec, { forceRecompile: true });
134
+ expect(result.success).toBe(true);
135
+ expect(result.fromCache).toBe(false);
136
+ expect(compiler.compile).toHaveBeenCalledTimes(1);
137
+ });
138
+ });
139
+
140
+ // --------------------------------------------------------------------------
141
+ // loadHierarchy – dependency resolution
142
+ // --------------------------------------------------------------------------
143
+ describe('loadHierarchy', () => {
144
+ it('should load root and its dependencies', async () => {
145
+ const childSpec = makeSpec({ name: 'ChildBtn', code: 'function ChildBtn() { return "child"; }' });
146
+ const rootSpec = makeSpec({
147
+ name: 'ParentForm',
148
+ code: 'function ParentForm() { return "parent"; }',
149
+ dependencies: [childSpec],
150
+ });
151
+
152
+ const result = await manager.loadHierarchy(rootSpec);
153
+
154
+ expect(result.success).toBe(true);
155
+ expect(result.loadedComponents).toContain('ParentForm');
156
+ expect(result.loadedComponents).toContain('ChildBtn');
157
+ expect(compiler.compile).toHaveBeenCalledTimes(2);
158
+ });
159
+
160
+ it('should use updated dependency code even when root is cached', async () => {
161
+ const childV1 = makeSpec({ name: 'Search', code: 'function Search() { return "v1"; }' });
162
+ const rootSpec = makeSpec({
163
+ name: 'Dashboard',
164
+ code: 'function Dashboard() { return "root"; }',
165
+ dependencies: [childV1],
166
+ });
167
+
168
+ // First load – populates cache
169
+ await manager.loadHierarchy(rootSpec);
170
+ expect(compiler.compile).toHaveBeenCalledTimes(2);
171
+ compiler.compile.mockClear();
172
+
173
+ // Second load – same root code, but updated Search dependency code
174
+ const childV2 = makeSpec({ name: 'Search', code: 'function Search() { return "v2"; }' });
175
+ const rootSpecUpdated = makeSpec({
176
+ name: 'Dashboard',
177
+ code: 'function Dashboard() { return "root"; }', // same root code
178
+ dependencies: [childV2],
179
+ });
180
+
181
+ const result = await manager.loadHierarchy(rootSpecUpdated);
182
+
183
+ expect(result.success).toBe(true);
184
+ // Root should be cached, Search should recompile with new code
185
+ expect(result.stats?.fromCache).toBe(1); // root only
186
+ expect(compiler.compile).toHaveBeenCalledTimes(1); // Search recompiled
187
+ // Verify the recompiled dependency used the v2 code
188
+ const compileCall = compiler.compile.mock.calls[0][0];
189
+ expect(compileCall.componentCode).toContain('return "v2"');
190
+ });
191
+
192
+ it('should cache dependencies individually by content hash', async () => {
193
+ const child = makeSpec({ name: 'Grid', code: 'function Grid() { return "grid"; }' });
194
+ const root1 = makeSpec({
195
+ name: 'Page1',
196
+ code: 'function Page1() { return "p1"; }',
197
+ dependencies: [child],
198
+ });
199
+ const root2 = makeSpec({
200
+ name: 'Page2',
201
+ code: 'function Page2() { return "p2"; }',
202
+ dependencies: [{ ...child }], // same Grid dependency
203
+ });
204
+
205
+ await manager.loadHierarchy(root1);
206
+ expect(compiler.compile).toHaveBeenCalledTimes(2); // Page1 + Grid
207
+ compiler.compile.mockClear();
208
+
209
+ // Loading Page2 with the exact same Grid dep should reuse Grid from cache
210
+ const result = await manager.loadHierarchy(root2);
211
+ expect(result.success).toBe(true);
212
+ expect(compiler.compile).toHaveBeenCalledTimes(1); // Only Page2 compiled, Grid cached
213
+ });
214
+
215
+ it('should handle deeply nested dependency updates', async () => {
216
+ const leafV1 = makeSpec({ name: 'Leaf', code: 'function Leaf() { return "leaf-v1"; }' });
217
+ const mid = makeSpec({
218
+ name: 'Middle',
219
+ code: 'function Middle() { return "mid"; }',
220
+ dependencies: [leafV1],
221
+ });
222
+ const root = makeSpec({
223
+ name: 'Root',
224
+ code: 'function Root() { return "root"; }',
225
+ dependencies: [mid],
226
+ });
227
+
228
+ // First load
229
+ await manager.loadHierarchy(root);
230
+ expect(compiler.compile).toHaveBeenCalledTimes(3);
231
+ compiler.compile.mockClear();
232
+
233
+ // Update only the leaf
234
+ const leafV2 = makeSpec({ name: 'Leaf', code: 'function Leaf() { return "leaf-v2"; }' });
235
+ const midUpdated = makeSpec({
236
+ name: 'Middle',
237
+ code: 'function Middle() { return "mid"; }',
238
+ dependencies: [leafV2],
239
+ });
240
+ const rootUpdated = makeSpec({
241
+ name: 'Root',
242
+ code: 'function Root() { return "root"; }',
243
+ dependencies: [midUpdated],
244
+ });
245
+
246
+ const result = await manager.loadHierarchy(rootUpdated);
247
+ expect(result.success).toBe(true);
248
+ // Root cached, Middle cached, only Leaf recompiled
249
+ expect(compiler.compile).toHaveBeenCalledTimes(1);
250
+ const compileCall = compiler.compile.mock.calls[0][0];
251
+ expect(compileCall.componentCode).toContain('leaf-v2');
252
+ });
253
+
254
+ it('should handle circular dependencies without infinite loop', async () => {
255
+ const specA = makeSpec({ name: 'CompA', code: 'function CompA() {}' });
256
+ const specB = makeSpec({
257
+ name: 'CompB',
258
+ code: 'function CompB() {}',
259
+ dependencies: [specA],
260
+ });
261
+ specA.dependencies = [specB];
262
+
263
+ const root = makeSpec({
264
+ name: 'Root',
265
+ code: 'function Root() {}',
266
+ dependencies: [specA],
267
+ });
268
+
269
+ const result = await manager.loadHierarchy(root);
270
+ expect(result.success).toBe(true);
271
+ expect(result.loadedComponents).toContain('Root');
272
+ });
273
+
274
+ it('should fallback to cached spec dependencies when input has none', async () => {
275
+ const child = makeSpec({ name: 'Chart', code: 'function Chart() {}' });
276
+ const fullRootSpec = makeSpec({
277
+ name: 'Dashboard',
278
+ code: 'function Dashboard() {}',
279
+ dependencies: [child],
280
+ });
281
+
282
+ // First load with full spec (has dependencies)
283
+ await manager.loadHierarchy(fullRootSpec);
284
+ expect(compiler.compile).toHaveBeenCalledTimes(2);
285
+ compiler.compile.mockClear();
286
+
287
+ // Second load with no dependencies field - simulates registry reference
288
+ const minimalSpec = makeSpec({
289
+ name: 'Dashboard',
290
+ code: 'function Dashboard() {}',
291
+ });
292
+ delete (minimalSpec as any).dependencies;
293
+
294
+ const result = await manager.loadHierarchy(minimalSpec);
295
+ expect(result.success).toBe(true);
296
+ expect(result.loadedComponents).toContain('Dashboard');
297
+ // Chart should still be loaded via cached spec's dependencies
298
+ expect(result.loadedComponents).toContain('Chart');
299
+ });
300
+
301
+ it('should prefer input dependencies over stale cached dependencies', async () => {
302
+ const depA = makeSpec({ name: 'DepA', code: 'function DepA() { return "A"; }' });
303
+ const root = makeSpec({
304
+ name: 'App',
305
+ code: 'function App() { return "app"; }',
306
+ dependencies: [depA],
307
+ });
308
+ await manager.loadHierarchy(root);
309
+ compiler.compile.mockClear();
310
+
311
+ // Same root code but dependency list changed (depB instead of depA)
312
+ const depB = makeSpec({ name: 'DepB', code: 'function DepB() { return "B"; }' });
313
+ const rootUpdated = makeSpec({
314
+ name: 'App',
315
+ code: 'function App() { return "app"; }',
316
+ dependencies: [depB],
317
+ });
318
+ const result = await manager.loadHierarchy(rootUpdated);
319
+
320
+ expect(result.success).toBe(true);
321
+ expect(result.loadedComponents).toContain('DepB');
322
+ expect(compiler.compile).toHaveBeenCalledTimes(1); // only DepB
323
+ });
324
+ });
325
+
326
+ // --------------------------------------------------------------------------
327
+ // Cache management
328
+ // --------------------------------------------------------------------------
329
+ describe('clearCache', () => {
330
+ it('should clear the fetch cache', async () => {
331
+ const spec = makeSpec({ name: 'Widget' });
332
+ await manager.loadComponent(spec);
333
+
334
+ manager.clearCache();
335
+
336
+ const stats = manager.getCacheStats();
337
+ expect(stats.fetchCacheSize).toBe(0);
338
+ });
339
+ });
340
+ });
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { ComponentSpec, ComponentLibraryDependency } from '@memberjunction/interactive-component-types';
7
7
  import { UserInfo, Metadata, LogError } from '@memberjunction/core';
8
- import { ComponentMetadataEngine, MJComponentLibraryEntity, MJComponentEntityExtended } from '@memberjunction/core-entities';
8
+ import { ComponentMetadataEngine, MJComponentLibraryEntity } from '@memberjunction/core-entities';
9
9
 
10
10
  import { ComponentCompiler } from '../compiler';
11
11
  import { ComponentRegistry } from '../registry';
@@ -84,6 +84,25 @@ export class ComponentManager {
84
84
  // Check if already loading to prevent duplicate work
85
85
  const existingPromise = this.loadingPromises.get(componentKey);
86
86
  if (existingPromise && !options.forceRefresh) {
87
+ // Before returning the pending promise, check if the compiled component
88
+ // is already in the registry. This prevents deadlock in circular dependency
89
+ // chains: A's Step 6 loads B, B's Step 6 loads A again — but A was already
90
+ // compiled and registered (Step 5 runs before Step 6). Returning the pending
91
+ // promise would deadlock because it's waiting on its own call chain.
92
+ const namespace = spec.namespace || options.defaultNamespace || 'Global';
93
+ const version = spec.version || options.defaultVersion || 'latest';
94
+ const contentHash = this.calculateHash(spec);
95
+ const registered = this.registry.get(spec.name, namespace, version, contentHash);
96
+ if (registered) {
97
+ this.log(`Component already registered (circular dep resolution): ${spec.name}`);
98
+ return {
99
+ success: true,
100
+ component: registered,
101
+ spec,
102
+ fromCache: true
103
+ };
104
+ }
105
+
87
106
  this.log(`Component already loading: ${spec.name}, waiting...`);
88
107
  return existingPromise;
89
108
  }
@@ -406,8 +425,13 @@ export class ComponentManager {
406
425
  }
407
426
 
408
427
  // Load dependencies
409
- if (result.spec?.dependencies) {
410
- for (const dep of result.spec.dependencies) {
428
+ // Prefer the input spec's dependencies over the cached result's dependencies.
429
+ // When a parent component is served from cache, result.spec contains stale
430
+ // dependency code. The input spec has the latest dependency code from the caller.
431
+ // Each dependency still gets its own individual cache check via loadComponent.
432
+ const dependencies = spec.dependencies || result.spec?.dependencies;
433
+ if (dependencies) {
434
+ for (const dep of dependencies) {
411
435
  // Normalize dependency spec for local registry lookup
412
436
  const depSpec = { ...dep };
413
437
  // OPTIMIZATION: If the dependency already has code (from registry population),
@@ -491,87 +515,82 @@ export class ComponentManager {
491
515
  // Check cache first
492
516
  const cacheKey = this.getComponentKey(spec, {});
493
517
  const cached = this.fetchCache.get(cacheKey);
494
-
518
+
495
519
  if (cached && this.isCacheValid(cached)) {
496
520
  this.log(`Using cached spec for: ${spec.name}`);
497
521
  return cached.spec;
498
522
  }
499
-
523
+
500
524
  // Handle LOCAL registry components (registry is null/undefined)
501
525
  if (!spec.registry) {
502
- this.log(`Fetching from local registry: ${spec.name}`);
503
-
504
- // Find component in local ComponentMetadataEngine
505
- const localComponent = this.componentEngine.Components?.find(
506
- (c: MJComponentEntityExtended) => {
507
- // Match by name (case-insensitive for better compatibility)
508
- const nameMatch = c.Name?.toLowerCase() === spec.name?.toLowerCase();
509
-
510
- // Match by namespace if provided (handle different formats)
511
- const namespaceMatch = !spec.namespace || c.Namespace?.toLowerCase() === spec.namespace?.toLowerCase();
526
+ const localComponent = await this.componentEngine.FindComponent(spec.name, spec.namespace);
512
527
 
513
- if (nameMatch && !namespaceMatch) {
514
- }
515
-
516
- return nameMatch && namespaceMatch;
517
- }
518
- );
519
-
520
528
  if (!localComponent) {
521
529
  throw new Error(`Local component not found: ${spec.name}`);
522
530
  }
523
-
524
- // Parse specification from local component
531
+
525
532
  if (!localComponent.Specification) {
526
533
  throw new Error(`Local component ${spec.name} has no specification`);
527
534
  }
528
-
535
+
529
536
  const fullSpec = JSON.parse(localComponent.Specification);
530
-
531
- // Cache it
537
+
532
538
  this.fetchCache.set(cacheKey, {
533
539
  spec: fullSpec,
534
540
  fetchedAt: new Date(),
535
541
  usageNotified: false
536
542
  });
537
-
543
+
538
544
  return fullSpec;
539
545
  }
540
-
546
+
541
547
  // Handle EXTERNAL registry components (registry has a name)
542
- // Initialize GraphQL client if needed
543
548
  if (!this.graphQLClient) {
544
549
  await this.initializeGraphQLClient();
545
550
  }
546
-
551
+
547
552
  if (!this.graphQLClient) {
548
553
  throw new Error('GraphQL client not available for registry fetching');
549
554
  }
550
-
551
- // Fetch from external registry
555
+
556
+ // Fetch from external registry, passing the cached hash (if any) so the
557
+ // server can return 304 Not Modified when the spec hasn't changed
552
558
  this.log(`Fetching from external registry: ${spec.registry}/${spec.name}`);
553
-
554
- const fullSpec = await this.graphQLClient.GetRegistryComponent({
559
+ const cachedHash = cached?.hash;
560
+
561
+ const response = await this.graphQLClient.GetRegistryComponentWithHash({
555
562
  registryName: spec.registry,
556
563
  namespace: spec.namespace || 'Global',
557
564
  name: spec.name,
558
- version: spec.version || 'latest'
565
+ version: spec.version || 'latest',
566
+ hash: cachedHash
559
567
  });
560
-
561
- if (!fullSpec) {
568
+
569
+ // If not modified (304), reuse the cached spec and refresh the TTL
570
+ if (response.notModified && cached) {
571
+ this.log(`Registry returned 304 for ${spec.name}, reusing cached spec`);
572
+ this.fetchCache.set(cacheKey, {
573
+ ...cached,
574
+ fetchedAt: new Date()
575
+ });
576
+ return cached.spec;
577
+ }
578
+
579
+ if (!response.specification) {
562
580
  throw new Error(`Component not found in registry: ${spec.registry}/${spec.name}`);
563
581
  }
564
-
565
- // Apply resolution mode if specified
582
+
583
+ const fullSpec = response.specification as ComponentSpec;
566
584
  const processedSpec = this.applyResolutionMode(fullSpec, spec, options?.resolutionMode);
567
-
568
- // Cache it
585
+
586
+ // Cache it with the registry hash for future 304 checks
569
587
  this.fetchCache.set(cacheKey, {
570
588
  spec: processedSpec,
571
589
  fetchedAt: new Date(),
590
+ hash: response.hash,
572
591
  usageNotified: false
573
592
  });
574
-
593
+
575
594
  return processedSpec;
576
595
  }
577
596
 
@@ -232,8 +232,8 @@ export class ComponentRegistryService {
232
232
  ): Promise<ComponentObject> {
233
233
  await this.initialize(contextUser);
234
234
 
235
- // Find component in metadata
236
- const component = this.componentEngine.Components.find((c: MJComponentEntity) => UUIDsEqual(c.ID, componentId));
235
+ // Find component in metadata via targeted query
236
+ const component = await this.componentEngine.FindComponentByID(componentId, contextUser);
237
237
  if (!component) {
238
238
  throw new Error(`Component not found: ${componentId}`);
239
239
  }
@@ -487,7 +487,7 @@ export class ComponentRegistryService {
487
487
  ): Promise<ComponentSpec> {
488
488
  await this.initialize(contextUser);
489
489
 
490
- const component = this.componentEngine.Components.find((c: MJComponentEntity) => UUIDsEqual(c.ID, componentId));
490
+ const component = await this.componentEngine.FindComponentByID(componentId, contextUser);
491
491
  if (!component) {
492
492
  throw new Error(`Component not found: ${componentId}`);
493
493
  }
@@ -697,16 +697,14 @@ export class ComponentRegistryService {
697
697
 
698
698
  for (const dep of dependencies) {
699
699
  // Find the dependency component
700
- const depComponent = this.componentEngine.Components.find(
701
- (c: MJComponentEntity) => UUIDsEqual(c.ID, dep.DependencyComponentID)
702
- );
703
-
700
+ const depComponent = await this.componentEngine.FindComponentByID(dep.DependencyComponentID, contextUser);
701
+
704
702
  if (depComponent) {
705
703
  result.push({
706
704
  name: depComponent.Name,
707
705
  namespace: depComponent.Namespace || '',
708
- version: depComponent.Version, // Version comes from the linked Component record
709
- isRequired: true, // All dependencies are required in MemberJunction
706
+ version: depComponent.Version,
707
+ isRequired: true,
710
708
  location: depComponent.SourceRegistryID ? 'registry' : 'embedded',
711
709
  sourceRegistryID: depComponent.SourceRegistryID
712
710
  });
@@ -734,27 +732,23 @@ export class ComponentRegistryService {
734
732
 
735
733
  await this.initialize(contextUser);
736
734
 
737
- const component = this.componentEngine.Components.find((c: MJComponentEntity) => UUIDsEqual(c.ID, componentId));
735
+ const component = await this.componentEngine.FindComponentByID(componentId, contextUser);
738
736
  if (!component) {
739
737
  return { componentId, dependencies: [] };
740
738
  }
741
-
739
+
742
740
  // Get direct dependencies
743
741
  const directDeps = await this.loadDependencies(componentId, contextUser);
744
-
742
+
745
743
  // Recursively resolve each dependency
746
744
  const dependencies: DependencyTree[] = [];
747
745
  for (const dep of directDeps) {
748
- // Find the dependency component
749
- const depComponent = this.componentEngine.Components.find(
750
- c => c.Name.trim().toLowerCase() === dep.name.trim().toLowerCase() &&
751
- c.Namespace?.trim().toLowerCase() === dep.namespace?.trim().toLowerCase()
752
- );
753
-
746
+ const depComponent = await this.componentEngine.FindComponent(dep.name, dep.namespace, undefined, contextUser);
747
+
754
748
  if (depComponent) {
755
749
  const subTree = await this.resolveDependencyTree(
756
- depComponent.ID,
757
- contextUser,
750
+ depComponent.ID,
751
+ contextUser,
758
752
  visited
759
753
  );
760
754
  dependencies.push(subTree);