@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
JavaScript
"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