@memberjunction/react-runtime 2.89.0 → 2.90.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.
@@ -12,6 +12,7 @@ import {
12
12
  ComponentError,
13
13
  RuntimeContext
14
14
  } from '../types';
15
+ import { LibraryRegistry } from '../utilities/library-registry';
15
16
 
16
17
  /**
17
18
  * Default compiler configuration
@@ -78,6 +79,9 @@ export class ComponentCompiler {
78
79
  // Validate inputs
79
80
  this.validateCompileOptions(options);
80
81
 
82
+ // Load required libraries if specified
83
+ const loadedLibraries = await this.loadRequiredLibraries(options.libraries);
84
+
81
85
  // Transpile the component code
82
86
  const transpiledCode = this.transpileComponent(
83
87
  options.componentCode,
@@ -85,10 +89,11 @@ export class ComponentCompiler {
85
89
  options
86
90
  );
87
91
 
88
- // Create the component factory
92
+ // Create the component factory with loaded libraries
89
93
  const componentFactory = this.createComponentFactory(
90
94
  transpiledCode,
91
- options.componentName
95
+ options.componentName,
96
+ loadedLibraries
92
97
  );
93
98
 
94
99
  // Build the compiled component
@@ -137,7 +142,7 @@ export class ComponentCompiler {
137
142
  throw new Error('Babel instance not set. Call setBabelInstance() first.');
138
143
  }
139
144
 
140
- const wrappedCode = this.wrapComponentCode(code, componentName);
145
+ const wrappedCode = this.wrapComponentCode(code, componentName, options.libraries);
141
146
 
142
147
  try {
143
148
  const result = this.babelInstance.transform(wrappedCode, {
@@ -158,16 +163,25 @@ export class ComponentCompiler {
158
163
  * Wraps component code in a factory function for execution
159
164
  * @param componentCode - Raw component code
160
165
  * @param componentName - Name of the component
166
+ * @param libraries - Optional library dependencies
161
167
  * @returns Wrapped component code
162
168
  */
163
- private wrapComponentCode(componentCode: string, componentName: string): string {
169
+ private wrapComponentCode(componentCode: string, componentName: string, libraries?: any[]): string {
170
+ // Generate library declarations if libraries are provided
171
+ const libraryDeclarations = libraries && libraries.length > 0
172
+ ? libraries
173
+ .filter(lib => lib.globalVariable) // Only include libraries with globalVariable
174
+ .map(lib => `const ${lib.globalVariable} = libraries['${lib.globalVariable}'];`)
175
+ .join('\n ')
176
+ : '';
177
+
164
178
  return `
165
179
  function createComponent(
166
180
  React, ReactDOM,
167
181
  useState, useEffect, useCallback, useMemo, useRef, useContext, useReducer, useLayoutEffect,
168
182
  libraries, styles, console
169
183
  ) {
170
- ${componentCode}
184
+ ${libraryDeclarations ? libraryDeclarations + '\n ' : ''}${componentCode}
171
185
 
172
186
  // Ensure the component exists
173
187
  if (typeof ${componentName} === 'undefined') {
@@ -190,13 +204,163 @@ export class ComponentCompiler {
190
204
  `;
191
205
  }
192
206
 
207
+ /**
208
+ * Load required libraries from the registry
209
+ * @param libraries - Array of library dependencies
210
+ * @returns Map of loaded libraries
211
+ */
212
+ private async loadRequiredLibraries(libraries?: any[]): Promise<Map<string, any>> {
213
+ const loadedLibraries = new Map<string, any>();
214
+
215
+ if (!libraries || libraries.length === 0) {
216
+ return loadedLibraries;
217
+ }
218
+
219
+ // Only works in browser environment
220
+ if (typeof window === 'undefined') {
221
+ console.warn('Library loading is only supported in browser environments');
222
+ return loadedLibraries;
223
+ }
224
+
225
+ const loadPromises = libraries.map(async (lib) => {
226
+ // Check if library is approved
227
+ if (!LibraryRegistry.isApproved(lib.name)) {
228
+ throw new Error(`Library '${lib.name}' is not approved. Only approved libraries can be used.`);
229
+ }
230
+
231
+ // Get library definition for complete info
232
+ const libraryDef = LibraryRegistry.getLibrary(lib.name);
233
+ if (!libraryDef) {
234
+ throw new Error(`Library '${lib.name}' not found in registry`);
235
+ }
236
+
237
+ // Get CDN URL for the library
238
+ const resolvedVersion = LibraryRegistry.resolveVersion(lib.name, lib.version);
239
+ const cdnUrl = LibraryRegistry.getCdnUrl(lib.name, resolvedVersion);
240
+
241
+ if (!cdnUrl) {
242
+ throw new Error(`No CDN URL found for library '${lib.name}' version '${lib.version || 'default'}'`);
243
+ }
244
+
245
+ // Check if already loaded
246
+ if ((window as any)[lib.globalVariable]) {
247
+ loadedLibraries.set(lib.globalVariable, (window as any)[lib.globalVariable]);
248
+ return;
249
+ }
250
+
251
+ // Load CSS files if the library requires them
252
+ const versionInfo = libraryDef.versions[resolvedVersion || libraryDef.defaultVersion];
253
+ if (versionInfo?.cssUrls) {
254
+ await this.loadStyles(versionInfo.cssUrls);
255
+ }
256
+
257
+ // Load the library dynamically (cdnUrl is guaranteed to be non-null here due to check above)
258
+ await this.loadScript(cdnUrl!, lib.globalVariable);
259
+
260
+ // Capture the library value from global scope
261
+ // Note: Libraries loaded from CDN typically attach to window automatically
262
+ // We capture them here to pass through the component's closure
263
+ const libraryValue = (window as any)[lib.globalVariable];
264
+ if (libraryValue) {
265
+ loadedLibraries.set(lib.globalVariable, libraryValue);
266
+ } else {
267
+ throw new Error(`Library '${lib.name}' failed to load or did not expose '${lib.globalVariable}'`);
268
+ }
269
+ });
270
+
271
+ await Promise.all(loadPromises);
272
+ return loadedLibraries;
273
+ }
274
+
275
+ /**
276
+ * Load CSS stylesheets dynamically
277
+ * @param urls - Array of CSS URLs to load
278
+ * @returns Promise that resolves when all stylesheets are loaded
279
+ */
280
+ private async loadStyles(urls: string[]): Promise<void> {
281
+ const loadPromises = urls.map(url => {
282
+ return new Promise<void>((resolve) => {
283
+ // Check if stylesheet already exists
284
+ const existingLink = document.querySelector(`link[href="${url}"]`);
285
+ if (existingLink) {
286
+ resolve();
287
+ return;
288
+ }
289
+
290
+ // Create new link element
291
+ const link = document.createElement('link');
292
+ link.rel = 'stylesheet';
293
+ link.href = url;
294
+
295
+ // CSS load events are not reliable cross-browser, so resolve immediately
296
+ // The CSS will load asynchronously but won't block component rendering
297
+ document.head.appendChild(link);
298
+ resolve();
299
+ });
300
+ });
301
+
302
+ await Promise.all(loadPromises);
303
+ }
304
+
305
+ /**
306
+ * Load a script dynamically
307
+ * @param url - Script URL
308
+ * @param globalName - Expected global variable name
309
+ * @returns Promise that resolves when script is loaded
310
+ */
311
+ private loadScript(url: string, globalName: string): Promise<void> {
312
+ return new Promise((resolve, reject) => {
313
+ // Check if script already exists
314
+ const existingScript = document.querySelector(`script[src="${url}"]`);
315
+ if (existingScript) {
316
+ // Wait for it to finish loading
317
+ const checkLoaded = () => {
318
+ if ((window as any)[globalName]) {
319
+ resolve();
320
+ } else {
321
+ setTimeout(checkLoaded, 100);
322
+ }
323
+ };
324
+ checkLoaded();
325
+ return;
326
+ }
327
+
328
+ // Create new script element
329
+ const script = document.createElement('script');
330
+ script.src = url;
331
+ script.async = true;
332
+
333
+ script.onload = () => {
334
+ // Give the library a moment to initialize
335
+ setTimeout(() => {
336
+ if ((window as any)[globalName]) {
337
+ resolve();
338
+ } else {
339
+ reject(new Error(`${globalName} not found after loading script`));
340
+ }
341
+ }, 100);
342
+ };
343
+
344
+ script.onerror = () => {
345
+ reject(new Error(`Failed to load script: ${url}`));
346
+ };
347
+
348
+ document.head.appendChild(script);
349
+ });
350
+ }
351
+
193
352
  /**
194
353
  * Creates a component factory function from transpiled code
195
354
  * @param transpiledCode - Transpiled JavaScript code
196
355
  * @param componentName - Name of the component
356
+ * @param loadedLibraries - Map of loaded libraries
197
357
  * @returns Component factory function
198
358
  */
199
- private createComponentFactory(transpiledCode: string, componentName: string): Function {
359
+ private createComponentFactory(
360
+ transpiledCode: string,
361
+ componentName: string,
362
+ loadedLibraries: Map<string, any>
363
+ ): Function {
200
364
  try {
201
365
  // Create the factory function with all React hooks
202
366
  const factoryCreator = new Function(
@@ -209,6 +373,12 @@ export class ComponentCompiler {
209
373
  // Return a function that executes the factory with runtime context
210
374
  return (context: RuntimeContext, styles: any = {}) => {
211
375
  const { React, ReactDOM, libraries = {} } = context;
376
+
377
+ // Merge loaded libraries with context libraries
378
+ const mergedLibraries = { ...libraries };
379
+ loadedLibraries.forEach((value, key) => {
380
+ mergedLibraries[key] = value;
381
+ });
212
382
 
213
383
  // Execute the factory creator to get the createComponent function
214
384
  const createComponentFn = factoryCreator(
@@ -222,7 +392,7 @@ export class ComponentCompiler {
222
392
  React.useContext,
223
393
  React.useReducer,
224
394
  React.useLayoutEffect,
225
- libraries,
395
+ mergedLibraries,
226
396
  styles,
227
397
  console
228
398
  );
@@ -239,7 +409,7 @@ export class ComponentCompiler {
239
409
  React.useContext,
240
410
  React.useReducer,
241
411
  React.useLayoutEffect,
242
- libraries,
412
+ mergedLibraries,
243
413
  styles,
244
414
  console
245
415
  );
@@ -366,7 +536,8 @@ export class ComponentCompiler {
366
536
  if (this.compilationCache.size >= this.config.maxCacheSize) {
367
537
  // Remove oldest entry (first in map)
368
538
  const firstKey = this.compilationCache.keys().next().value;
369
- this.compilationCache.delete(firstKey);
539
+ if (firstKey)
540
+ this.compilationCache.delete(firstKey);
370
541
  }
371
542
 
372
543
  const cacheKey = this.createCacheKey(component.name, code);
@@ -160,7 +160,8 @@ export class ComponentHierarchyRegistrar {
160
160
  const compileOptions: CompileOptions = {
161
161
  componentName: spec.name,
162
162
  componentCode: spec.code,
163
- styles
163
+ styles,
164
+ libraries: spec.libraries // Pass along library dependencies from the spec
164
165
  };
165
166
 
166
167
  const compilationResult = await this.compiler.compile(compileOptions);
@@ -36,6 +36,8 @@ export interface CompileOptions {
36
36
  babelPlugins?: string[];
37
37
  /** Custom Babel presets to use */
38
38
  babelPresets?: string[];
39
+ /** Library dependencies that the component requires */
40
+ libraries?: any[]; // Using any[] to avoid circular dependency with InteractiveComponents
39
41
  }
40
42
 
41
43
  /**
@@ -7,6 +7,7 @@ export * from './runtime-utilities';
7
7
  export * from './component-styles';
8
8
  export * from './standard-libraries';
9
9
  export * from './library-loader';
10
+ export * from './library-registry';
10
11
  export * from './component-error-analyzer';
11
12
  export * from './resource-manager';
12
13
  export * from './cache-manager';