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.

675 lines (654 loc) â€ĸ 32.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ComponentCompiler = void 0; const library_registry_1 = require("../utilities/library-registry"); const library_loader_1 = require("../utilities/library-loader"); const component_unwrapper_1 = require("../utilities/component-unwrapper"); const DEFAULT_COMPILER_CONFIG = { babel: { presets: ['react'], plugins: [] }, minify: false, sourceMaps: false, cache: true, maxCacheSize: 100, debug: false }; class ComponentCompiler { constructor(config) { this.CORE_LIBRARIES = new Set(['React', 'ReactDOM']); 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, options); const compiledComponent = { factory: 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, loadedLibraries: loadedLibraries }; } 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, options.dependencies); 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, dependencies) { const debug = this.config.debug; const libraryDeclarations = libraries && libraries.length > 0 ? libraries .filter(lib => lib.globalVariable && !this.CORE_LIBRARIES.has(lib.globalVariable)) .map(lib => `const ${lib.globalVariable} = libraries['${lib.globalVariable}'];`) .join('\n ') : ''; const libraryLogChecks = libraries && libraries.length > 0 ? libraries .filter(lib => lib.globalVariable && !this.CORE_LIBRARIES.has(lib.globalVariable)) .map(lib => `\nif (!${lib.globalVariable}) { console.error('[React-Runtime-JS] Library "${lib.globalVariable}" is not defined'); } else { ${debug ? `console.log('[React-Runtime-JS] Library "${lib.globalVariable}" is defined');` : ''} }`) .join('\n ') : ''; const seenDependencies = new Set(); const uniqueDependencies = []; const duplicates = []; if (dependencies && dependencies.length > 0) { for (const dep of dependencies) { if (dep.name === componentName) { continue; } if (seenDependencies.has(dep.name)) { duplicates.push(dep.name); } else { seenDependencies.add(dep.name); uniqueDependencies.push(dep); } } } const duplicateWarnings = duplicates.length > 0 ? duplicates .map(name => `console.warn('[React-Runtime-JS] WARNING: Component "${name}" is registered multiple times as a dependency. Using first occurrence only.');`) .join('\n ') : ''; const componentDeclarations = uniqueDependencies.length > 0 ? uniqueDependencies .map(dep => `const ${dep.name}Raw = componentsOuter['${dep.name}']; ${debug ? `console.log('[React-Runtime-JS] Extracting ${dep.name}:'); console.log(' - Raw value type:', typeof ${dep.name}Raw); console.log(' - Raw value:', ${dep.name}Raw); if (${dep.name}Raw && typeof ${dep.name}Raw === 'object') { console.log(' - Has .component property:', 'component' in ${dep.name}Raw); console.log(' - .component type:', typeof ${dep.name}Raw.component); }` : ''} const ${dep.name} = ${dep.name}Raw?.component || ${dep.name}Raw; ${debug ? `console.log(' - Final ${dep.name} type:', typeof ${dep.name}); console.log(' - Final ${dep.name} is function:', typeof ${dep.name} === 'function');` : ''}`) .join('\n ') : ''; const componentLogChecks = uniqueDependencies.length > 0 ? uniqueDependencies .map(dep => `if (!${dep.name}) { console.error('[React-Runtime-JS] Dependency "${dep.name}" is not defined'); } else { ${debug ? `console.log('[React-Runtime-JS] Dependency "${dep.name}" is defined');` : ''} }`) .join('\n ') : ''; const wrappedCode = ` function createComponent( React, ReactDOM, useState, useEffect, useCallback, useMemo, useRef, useContext, useReducer, useLayoutEffect, libraries, styles, console, components, unwrapLibraryComponent, unwrapLibraryComponents, unwrapAllLibraryComponents ) { if (!React) console.log('[React-Runtime-JS] React is not defined'); if (!ReactDOM) console.log('[React-Runtime-JS] ReactDOM is not defined'); // Make unwrap functions available with legacy names for backward compatibility const unwrapComponent = unwrapLibraryComponent; const unwrapComponents = unwrapLibraryComponents; const unwrapAllComponents = unwrapAllLibraryComponents; // Code for ${componentName} ${componentCode} // Ensure the component exists if (typeof ${componentName} === 'undefined') { throw new Error('Component "${componentName}" is not defined in the provided code'); } else { ${debug ? `console.log('[React-Runtime-JS] Component "${componentName}" is defined');` : ''} } // Store the component in a variable so we don't lose it const UserComponent = ${componentName}; // Check if the component is already a ComponentObject (has a .component property) // If so, extract the actual React component const ActualComponent = (typeof UserComponent === 'object' && UserComponent !== null && 'component' in UserComponent) ? UserComponent.component : UserComponent; // Debug logging to understand what we're getting ${debug ? ` console.log('[React-Runtime-JS]Component ${componentName} type:', typeof UserComponent); if (typeof UserComponent === 'object' && UserComponent !== null) { console.log('[React-Runtime-JS]Component ${componentName} keys:', Object.keys(UserComponent)); console.log('[React-Runtime-JS]Component ${componentName} has .component:', 'component' in UserComponent); if ('component' in UserComponent) { console.log('[React-Runtime-JS]Component ${componentName}.component type:', typeof UserComponent.component); } }` : ''} // Validate that we have a function (React component) if (typeof ActualComponent !== 'function') { console.error('[React-Runtime-JS] Invalid component type for ${componentName}:', typeof ActualComponent); console.error('[React-Runtime-JS] ActualComponent value:', ActualComponent); console.error('[React-Runtime-JS] Original UserComponent value:', UserComponent); throw new Error('[React-Runtime-JS] Component "${componentName}" must be a function (React component) or an object with a .component property that is a function. Got: ' + typeof ActualComponent); } let componentsOuter = null, utilitiesOuter = null; const DestructureWrapperUserComponent = (props) => { if (!componentsOuter) { componentsOuter = props?.components || components; } if (!utilitiesOuter) { utilitiesOuter = props?.utilities; } ${debug ? ` console.log('[React-Runtime-JS] DestructureWrapperUserComponent for ${componentName}:'); console.log(' - Props:', props); console.log(' - componentsOuter type:', typeof componentsOuter); console.log(' - componentsOuter:', componentsOuter); if (componentsOuter && typeof componentsOuter === 'object') { console.log(' - componentsOuter keys:', Object.keys(componentsOuter)); for (const key of Object.keys(componentsOuter)) { const comp = componentsOuter[key]; console.log(\` - componentsOuter[\${key}] type:\`, typeof comp); if (comp && typeof comp === 'object') { console.log(\` - Has .component: \${'component' in comp}\`); console.log(\` - .component type: \${typeof comp.component}\`); } } } console.log(' - styles:', styles); console.log(' - utilities:', utilitiesOuter); console.log(' - libraries:', libraries);` : ''} ${duplicateWarnings ? '// Duplicate dependency warnings\n ' + duplicateWarnings + '\n ' : ''} ${libraryDeclarations ? '// Destructure Libraries\n' + libraryDeclarations + '\n ' : ''} ${componentDeclarations ? '// Destructure Dependencies\n' + componentDeclarations + '\n ' : ''} ${libraryLogChecks} ${componentLogChecks} const newProps = { ...props, components: componentsOuter, utilities: utilitiesOuter } return ActualComponent(newProps); }; // Create a fresh method registry for each factory call const methodRegistry = new Map(); // Create a wrapper component that provides RegisterMethod in callbacks const ComponentWithMethodRegistry = (props) => { // Register methods on mount React.useEffect(() => { // Clear previous methods methodRegistry.clear(); // Provide RegisterMethod callback if callbacks exist if (props.callbacks && typeof props.callbacks.RegisterMethod === 'function') { // Component can now register its methods // This will be called from within the component } }, [props.callbacks]); // Create enhanced callbacks with RegisterMethod const enhancedCallbacks = React.useMemo(() => { if (!props.callbacks) return {}; return { ...props.callbacks, RegisterMethod: (methodName, handler) => { if (methodName && handler) { methodRegistry.set(methodName, handler); } } }; }, [props.callbacks]); // Render the original component with enhanced callbacks return React.createElement(DestructureWrapperUserComponent, { ...props, callbacks: enhancedCallbacks }); }; ComponentWithMethodRegistry.displayName = '${componentName}WithMethods'; // Return the component object with method access return { component: ComponentWithMethodRegistry, print: function() { const printMethod = methodRegistry.get('print'); if (printMethod) { printMethod(); } else if (typeof window !== 'undefined' && window.print) { window.print(); } }, refresh: function(data) { const refreshMethod = methodRegistry.get('refresh'); if (refreshMethod) { refreshMethod(data); } // Refresh functionality is handled by the host environment }, // Standard method accessors with type safety getCurrentDataState: function() { const method = methodRegistry.get('getCurrentDataState'); return method ? method() : undefined; }, getDataStateHistory: function() { const method = methodRegistry.get('getDataStateHistory'); return method ? method() : []; }, validate: function() { const method = methodRegistry.get('validate'); return method ? method() : true; }, isDirty: function() { const method = methodRegistry.get('isDirty'); return method ? method() : false; }, reset: function() { const method = methodRegistry.get('reset'); if (method) method(); }, scrollTo: function(target) { const method = methodRegistry.get('scrollTo'); if (method) method(target); }, focus: function(target) { const method = methodRegistry.get('focus'); if (method) method(target); }, // Generic method invoker for custom methods invokeMethod: function(methodName, ...args) { const method = methodRegistry.get(methodName); if (method) { return method(...args); } console.warn(\`[React-Runtime-JS] Method '\${methodName}' is not registered on component ${componentName}\`); return undefined; }, // Check if a method exists hasMethod: function(methodName) { return methodRegistry.has(methodName); } }; } `; return wrappedCode; } async loadRequiredLibraries(libraries, componentLibraries) { const loadedLibraries = new Map(); if (this.config.debug) { 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) { if (this.config.debug) { 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 filteredLibraries = libraries.filter(lib => { if (!lib || typeof lib !== 'object' || !lib.name) { console.warn(`âš ī¸ Invalid library entry detected (missing name):`, lib); return false; } if (lib.name === 'unknown' || lib.name === 'null' || lib.name === 'undefined') { console.warn(`âš ī¸ Filtering out invalid library with name '${lib.name}':`, lib); return false; } if (!lib.globalVariable || lib.globalVariable === 'undefined' || lib.globalVariable === 'null') { console.warn(`âš ī¸ Filtering out library '${lib.name}' with invalid globalVariable:`, lib.globalVariable); return false; } const libNameLower = lib.name.toLowerCase(); if (libNameLower === 'react' || libNameLower === 'reactdom') { console.warn(`âš ī¸ Library '${lib.name}' is automatically loaded by the React runtime and should not be requested separately`); return false; } return true; }); const libraryNames = filteredLibraries .map(lib => lib.name) .filter(name => name && typeof name === 'string'); if (this.config.debug) { console.log('đŸ“Ļ Using dependency-aware loading for libraries:', libraryNames); } if (filteredLibraries.length === 0) { if (this.config.debug) { console.log('📚 All requested libraries were filtered out (React/ReactDOM), returning empty map'); } return loadedLibraries; } try { const loadedLibraryMap = await library_loader_1.LibraryLoader.loadLibrariesWithDependencies(libraryNames, componentLibraries, 'component-compiler', { debug: this.config.debug }); for (const lib of filteredLibraries) { const isApproved = library_registry_1.LibraryRegistry.isApproved(lib.name); 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 loadedValue = loadedLibraryMap.get(lib.name); if (loadedValue) { loadedLibraries.set(lib.globalVariable, loadedValue); if (this.config.debug) { console.log(`✅ Mapped ${lib.name} to global variable ${lib.globalVariable}`); } } else { const globalValue = window[lib.globalVariable]; if (globalValue) { loadedLibraries.set(lib.globalVariable, globalValue); if (this.config.debug) { console.log(`✅ Found ${lib.name} already loaded as ${lib.globalVariable}`); } } else { console.error(`❌ Library '${lib.name}' failed to load`); throw new Error(`Library '${lib.name}' failed to load or did not expose '${lib.globalVariable}'`); } } } } catch (error) { console.error('Failed to load libraries with dependencies:', error); if (this.config.debug) { console.warn('âš ī¸ Falling back to non-dependency-aware loading due to error'); } for (const lib of libraries) { if (window[lib.globalVariable]) { loadedLibraries.set(lib.globalVariable, window[lib.globalVariable]); } else { const libraryDef = library_registry_1.LibraryRegistry.getLibrary(lib.name); if (libraryDef) { const resolvedVersion = library_registry_1.LibraryRegistry.resolveVersion(lib.name, lib.version); const cdnUrl = library_registry_1.LibraryRegistry.getCdnUrl(lib.name, resolvedVersion); if (cdnUrl) { await this.loadScript(cdnUrl, lib.globalVariable); const libraryValue = window[lib.globalVariable]; if (libraryValue) { loadedLibraries.set(lib.globalVariable, libraryValue); } } } } } } if (this.config.debug) { 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]) { if (this.config.debug) { console.log(` ✓ Global variable ${globalName} found after ${attempts * checkInterval}ms`); } resolve(); } else if (attempts >= maxAttempts) { if (this.config.debug) { console.error(` ❌ ${globalName} not found after ${attempts * checkInterval}ms`); const matchingKeys = Object.keys(window).filter(k => k.toLowerCase().includes(globalName.toLowerCase())); console.log(` â„šī¸ Matching window properties: ${matchingKeys.join(', ') || 'none'}`); } 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, options) { try { const factoryCreator = new Function('React', 'ReactDOM', 'useState', 'useEffect', 'useCallback', 'useMemo', 'useRef', 'useContext', 'useReducer', 'useLayoutEffect', 'libraries', 'styles', 'console', 'components', 'unwrapLibraryComponent', 'unwrapLibraryComponents', 'unwrapAllLibraryComponents', `${transpiledCode}; return createComponent;`); return (context, styles = {}, components = {}) => { const { React, ReactDOM, libraries = {} } = context; if (!React) { console.error('🔴 CRITICAL: React is NULL in createComponentFactory!'); console.error('Context provided:', context); console.error('Context keys:', Object.keys(context)); throw new Error('React is null in runtime context when creating component factory'); } if (!React.useState || !React.useEffect) { console.error('🔴 CRITICAL: React hooks are missing!'); console.error('React object keys:', React ? Object.keys(React) : 'React is null'); console.error('useState:', typeof React?.useState); console.error('useEffect:', typeof React?.useEffect); } const mergedLibraries = { ...libraries }; const specLibraryNames = new Set((options.libraries || []).map((lib) => lib.globalVariable).filter(Boolean)); loadedLibraries.forEach((value, key) => { if (specLibraryNames.has(key)) { mergedLibraries[key] = value; } else if (this.config.debug) { console.log(`âš ī¸ Filtering out dependency-only library: ${key}`); } }); const boundUnwrapLibraryComponent = (lib, name) => (0, component_unwrapper_1.unwrapLibraryComponent)(lib, name, this.config.debug); const boundUnwrapLibraryComponents = (lib, ...names) => (0, component_unwrapper_1.unwrapLibraryComponents)(lib, ...names, this.config.debug); const boundUnwrapAllLibraryComponents = (lib) => (0, component_unwrapper_1.unwrapAllLibraryComponents)(lib, this.config.debug); let createComponentFn; try { createComponentFn = factoryCreator(React, ReactDOM, React.useState, React.useEffect, React.useCallback, React.useMemo, React.useRef, React.useContext, React.useReducer, React.useLayoutEffect, mergedLibraries, styles, console, components, boundUnwrapLibraryComponent, boundUnwrapLibraryComponents, boundUnwrapAllLibraryComponents); } catch (error) { console.error('🔴 CRITICAL: Error calling factoryCreator with React hooks!'); console.error('Error:', error?.message || error); console.error('React is:', React); console.error('React type:', typeof React); if (React) { console.error('React.useState:', typeof React.useState); console.error('React.useEffect:', typeof React.useEffect); } throw new Error(`Failed to create component factory: ${error?.message || error}`); } const Component = createComponentFn(React, ReactDOM, React.useState, React.useEffect, React.useCallback, React.useMemo, React.useRef, React.useContext, React.useReducer, React.useLayoutEffect, mergedLibraries, styles, console, components, boundUnwrapLibraryComponent, boundUnwrapLibraryComponents, boundUnwrapAllLibraryComponents); 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