UNPKG

@memberjunction/react-runtime

Version:

Platform-agnostic React component runtime for MemberJunction. Provides core compilation, registry, and execution capabilities for React components in any JavaScript environment.

355 lines (353 loc) â€ĸ 16.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ComponentCompiler = void 0; const library_registry_1 = require("../utilities/library-registry"); const DEFAULT_COMPILER_CONFIG = { babel: { presets: ['react'], plugins: [] }, minify: false, sourceMaps: false, cache: true, maxCacheSize: 100 }; class ComponentCompiler { constructor(config) { this.config = { ...DEFAULT_COMPILER_CONFIG, ...config }; this.compilationCache = new Map(); } setBabelInstance(babel) { this.babelInstance = babel; } async compile(options) { const startTime = Date.now(); try { if (this.config.cache) { const cached = this.getCachedComponent(options.componentName, options.componentCode); if (cached) { return { success: true, component: cached, duration: Date.now() - startTime }; } } this.validateCompileOptions(options); const loadedLibraries = await this.loadRequiredLibraries(options.libraries, options.allLibraries); const transpiledCode = this.transpileComponent(options.componentCode, options.componentName, options); const componentFactory = this.createComponentFactory(transpiledCode, options.componentName, loadedLibraries); const compiledComponent = { component: componentFactory, id: this.generateComponentId(options.componentName), name: options.componentName, compiledAt: new Date(), warnings: [] }; if (this.config.cache) { this.cacheComponent(compiledComponent, options.componentCode); } return { success: true, component: compiledComponent, duration: Date.now() - startTime, size: transpiledCode.length }; } catch (error) { return { success: false, error: this.createCompilationError(error, options.componentName), duration: Date.now() - startTime }; } } transpileComponent(code, componentName, options) { if (!this.babelInstance) { throw new Error('Babel instance not set. Call setBabelInstance() first.'); } const wrappedCode = this.wrapComponentCode(code, componentName, options.libraries); try { const result = this.babelInstance.transform(wrappedCode, { presets: options.babelPresets || this.config.babel.presets, plugins: options.babelPlugins || this.config.babel.plugins, filename: `${componentName}.jsx`, sourceMaps: this.config.sourceMaps, minified: this.config.minify }); return result.code; } catch (error) { throw new Error(`Transpilation failed: ${error.message}`); } } wrapComponentCode(componentCode, componentName, libraries) { const libraryDeclarations = libraries && libraries.length > 0 ? libraries .filter(lib => lib.globalVariable) .map(lib => `const ${lib.globalVariable} = libraries['${lib.globalVariable}'];`) .join('\n ') : ''; return ` function createComponent( React, ReactDOM, useState, useEffect, useCallback, useMemo, useRef, useContext, useReducer, useLayoutEffect, libraries, styles, console ) { ${libraryDeclarations ? libraryDeclarations + '\n ' : ''}${componentCode} // Ensure the component exists if (typeof ${componentName} === 'undefined') { throw new Error('Component "${componentName}" is not defined in the provided code'); } // Return the component with utilities return { component: ${componentName}, print: function() { if (typeof window !== 'undefined' && window.print) { window.print(); } }, refresh: function(data) { // Refresh functionality is handled by the host environment } }; } `; } async loadRequiredLibraries(libraries, componentLibraries) { const loadedLibraries = new Map(); console.log('🔍 loadRequiredLibraries called with:', { librariesCount: libraries?.length || 0, libraries: libraries?.map(l => ({ name: l.name, version: l.version, globalVariable: l.globalVariable })) }); if (!libraries || libraries.length === 0) { console.log('📚 No libraries to load, returning empty map'); return loadedLibraries; } if (typeof window === 'undefined') { console.warn('Library loading is only supported in browser environments'); return loadedLibraries; } if (componentLibraries) { await library_registry_1.LibraryRegistry.Config(false, componentLibraries); } else { console.warn('âš ī¸ No componentLibraries provided for LibraryRegistry initialization'); } const loadPromises = libraries.map(async (lib) => { console.log(`đŸ“Ļ Processing library: ${lib.name}`); const isApproved = library_registry_1.LibraryRegistry.isApproved(lib.name); console.log(` ✓ Approved check for ${lib.name}: ${isApproved}`); if (!isApproved) { console.error(` ❌ Library '${lib.name}' is not approved`); throw new Error(`Library '${lib.name}' is not approved. Only approved libraries can be used.`); } const libraryDef = library_registry_1.LibraryRegistry.getLibrary(lib.name); console.log(` ✓ Library definition found for ${lib.name}: ${!!libraryDef}`); if (!libraryDef) { console.error(` ❌ Library '${lib.name}' not found in registry`); throw new Error(`Library '${lib.name}' not found in registry`); } const resolvedVersion = library_registry_1.LibraryRegistry.resolveVersion(lib.name, lib.version); console.log(` ✓ Resolved version for ${lib.name}: ${resolvedVersion}`); const cdnUrl = library_registry_1.LibraryRegistry.getCdnUrl(lib.name, resolvedVersion); console.log(` ✓ CDN URL for ${lib.name}: ${cdnUrl}`); if (!cdnUrl) { console.error(` ❌ No CDN URL found for library '${lib.name}' version '${lib.version || 'default'}'`); throw new Error(`No CDN URL found for library '${lib.name}' version '${lib.version || 'default'}'`); } if (window[lib.globalVariable]) { console.log(` â„šī¸ Library ${lib.name} already loaded globally as ${lib.globalVariable}`); loadedLibraries.set(lib.globalVariable, window[lib.globalVariable]); return; } const versionInfo = libraryDef.versions[resolvedVersion || libraryDef.defaultVersion]; if (versionInfo?.cssUrls) { await this.loadStyles(versionInfo.cssUrls); } console.log(` đŸ“Ĩ Loading script from CDN for ${lib.name}...`); await this.loadScript(cdnUrl, lib.globalVariable); const libraryValue = window[lib.globalVariable]; console.log(` ✓ Library ${lib.name} loaded successfully, global variable ${lib.globalVariable} is:`, typeof libraryValue); if (libraryValue) { loadedLibraries.set(lib.globalVariable, libraryValue); console.log(` ✅ Added ${lib.name} to loaded libraries map`); } else { console.error(` ❌ Library '${lib.name}' failed to expose global variable '${lib.globalVariable}'`); throw new Error(`Library '${lib.name}' failed to load or did not expose '${lib.globalVariable}'`); } }); await Promise.all(loadPromises); console.log(`✅ All libraries loaded successfully. Total: ${loadedLibraries.size}`); console.log('📚 Loaded libraries map:', Array.from(loadedLibraries.keys())); return loadedLibraries; } async loadStyles(urls) { const loadPromises = urls.map(url => { return new Promise((resolve) => { const existingLink = document.querySelector(`link[href="${url}"]`); if (existingLink) { resolve(); return; } const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = url; document.head.appendChild(link); resolve(); }); }); await Promise.all(loadPromises); } loadScript(url, globalName) { return new Promise((resolve, reject) => { const existingScript = document.querySelector(`script[src="${url}"]`); if (existingScript) { let attempts = 0; const maxAttempts = 50; const checkLoaded = () => { if (window[globalName]) { resolve(); } else if (attempts >= maxAttempts) { reject(new Error(`${globalName} not found after ${maxAttempts * 100}ms waiting for existing script`)); } else { attempts++; setTimeout(checkLoaded, 100); } }; checkLoaded(); return; } const script = document.createElement('script'); script.src = url; script.async = true; script.onload = () => { let attempts = 0; const maxAttempts = 20; const checkInterval = 100; const checkGlobal = () => { if (window[globalName]) { console.log(` ✓ Global variable ${globalName} found after ${attempts * checkInterval}ms`); resolve(); } else if (attempts >= maxAttempts) { console.error(` ❌ ${globalName} not found after ${attempts * checkInterval}ms`); console.log(` â„šī¸ Window properties:`, Object.keys(window).filter(k => k.toLowerCase().includes(globalName.toLowerCase()))); reject(new Error(`${globalName} not found after loading script from ${url}`)); } else { attempts++; setTimeout(checkGlobal, checkInterval); } }; checkGlobal(); }; script.onerror = () => { reject(new Error(`Failed to load script: ${url}`)); }; document.head.appendChild(script); }); } createComponentFactory(transpiledCode, componentName, loadedLibraries) { try { const factoryCreator = new Function('React', 'ReactDOM', 'useState', 'useEffect', 'useCallback', 'useMemo', 'useRef', 'useContext', 'useReducer', 'useLayoutEffect', 'libraries', 'styles', 'console', `${transpiledCode}; return createComponent;`); return (context, styles = {}) => { const { React, ReactDOM, libraries = {} } = context; const mergedLibraries = { ...libraries }; loadedLibraries.forEach((value, key) => { mergedLibraries[key] = value; }); const createComponentFn = factoryCreator(React, ReactDOM, React.useState, React.useEffect, React.useCallback, React.useMemo, React.useRef, React.useContext, React.useReducer, React.useLayoutEffect, mergedLibraries, styles, console); const Component = createComponentFn(React, ReactDOM, React.useState, React.useEffect, React.useCallback, React.useMemo, React.useRef, React.useContext, React.useReducer, React.useLayoutEffect, mergedLibraries, styles, console); return Component; }; } catch (error) { throw new Error(`Failed to create component factory: ${error.message}`); } } validateCompileOptions(options) { if (!options) { throw new Error('Component compilation failed: No options provided.\n' + 'Expected an object with componentName and componentCode properties.\n' + 'Example: { componentName: "MyComponent", componentCode: "function MyComponent() { ... }" }'); } if (!options.componentName) { const providedKeys = Object.keys(options).join(', '); throw new Error('Component compilation failed: Component name is required.\n' + `Received options with keys: [${providedKeys}]\n` + 'Please ensure your component spec includes a "name" property.\n' + 'Example: { name: "MyComponent", code: "..." }'); } if (!options.componentCode) { throw new Error(`Component compilation failed: Component code is required for "${options.componentName}".\n` + 'Please ensure your component spec includes a "code" property with the component source code.\n' + 'Example: { name: "MyComponent", code: "function MyComponent() { return <div>Hello</div>; }" }'); } if (typeof options.componentCode !== 'string') { const actualType = typeof options.componentCode; throw new Error(`Component compilation failed: Component code must be a string for "${options.componentName}".\n` + `Received type: ${actualType}\n` + `Received value: ${JSON.stringify(options.componentCode).substring(0, 100)}...\n` + 'Please ensure the code property contains a string of JavaScript/JSX code.'); } if (options.componentCode.trim().length === 0) { throw new Error(`Component compilation failed: Component code is empty for "${options.componentName}".\n` + 'The code property must contain valid JavaScript/JSX code defining a React component.'); } if (!options.componentCode.includes(options.componentName)) { throw new Error(`Component compilation failed: Component code must define a component named "${options.componentName}".\n` + 'The function/component name in the code must match the componentName property.\n' + `Expected to find: function ${options.componentName}(...) or const ${options.componentName} = ...\n` + 'Code preview: ' + options.componentCode.substring(0, 200) + '...'); } } generateComponentId(componentName) { return `${componentName}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } getCachedComponent(componentName, code) { const cacheKey = this.createCacheKey(componentName, code); return this.compilationCache.get(cacheKey); } createCacheKey(componentName, code) { let hash = 0; for (let i = 0; i < code.length; i++) { const char = code.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; } return `${componentName}_${hash.toString(36)}`; } cacheComponent(component, code) { if (this.compilationCache.size >= this.config.maxCacheSize) { const firstKey = this.compilationCache.keys().next().value; if (firstKey) this.compilationCache.delete(firstKey); } const cacheKey = this.createCacheKey(component.name, code); this.compilationCache.set(cacheKey, component); } createCompilationError(error, componentName) { return { message: error.message || 'Unknown compilation error', stack: error.stack, componentName, phase: 'compilation', details: error }; } clearCache() { this.compilationCache.clear(); } getCacheSize() { return this.compilationCache.size; } updateConfig(config) { this.config = { ...this.config, ...config }; } } exports.ComponentCompiler = ComponentCompiler; //# sourceMappingURL=component-compiler.js.map