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