import { readFileSync, writeFileSync, mkdirSync, rmSync, existsSync } from 'fs';
import { join, dirname, basename, resolve } from 'path';
import { tmpdir } from 'os';
import { randomUUID } from 'crypto';
import * as esbuild from 'esbuild';
export interface RenderOptions {
width?: number;
height?: number;
backgroundColor?: string;
props?: Record<string, unknown>;
}
const DEFAULT_OPTIONS: Required<RenderOptions> = {
width: 800,
height: 600,
backgroundColor: '#ffffff',
props: {
label: 'Button Text',
onClick: () => {},
children: 'Sample Content'
}
};
/**
* Creates an HTML file that renders a React component
*/
function createComponentHTML(
componentCode: string,
options: Required<RenderOptions>
): string {
const propsJson = JSON.stringify(options.props);
return `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@3/dist/tailwind.min.css" rel="stylesheet">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: ${options.backgroundColor};
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
#root {
display: inline-block;
}
</style>
</head>
<body>
<div id="root"></div>
<script>
// Component code (bundled)
${componentCode}
// Render the component
const props = ${propsJson};
props.onClick = () => {};
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(React.createElement(Component, props));
</script>
</body>
</html>`;
}
/**
* Creates a shim that provides React as a global
*/
function createReactShim(tempDir: string): string {
const shimCode = `
// React shim - use globals from CDN
export default window.React;
export const useState = window.React.useState;
export const useEffect = window.React.useEffect;
export const useCallback = window.React.useCallback;
export const useMemo = window.React.useMemo;
export const useRef = window.React.useRef;
export const useContext = window.React.useContext;
export const createContext = window.React.createContext;
export const createElement = window.React.createElement;
export const Fragment = window.React.Fragment;
export const Component = window.React.Component;
export const PureComponent = window.React.PureComponent;
export const memo = window.React.memo;
export const forwardRef = window.React.forwardRef;
export const Children = window.React.Children;
export const cloneElement = window.React.cloneElement;
export const isValidElement = window.React.isValidElement;
`;
const shimPath = join(tempDir, 'react-shim.js');
writeFileSync(shimPath, shimCode);
return shimPath;
}
/**
* Creates a wrapper module that exports the component as a global
*/
function createBundleEntry(componentPath: string, tempDir: string): string {
const entryCode = `
import ComponentModule from '${componentPath}';
window.Component = ComponentModule.default || ComponentModule;
`;
const entryPath = join(tempDir, 'entry.tsx');
writeFileSync(entryPath, entryCode);
return entryPath;
}
/**
* Bundle a React component file for browser usage
*/
export async function bundleComponent(
componentPath: string,
tempDir: string
): Promise<string> {
const absComponentPath = resolve(componentPath);
const entryPath = createBundleEntry(absComponentPath, tempDir);
const reactShimPath = createReactShim(tempDir);
const outputPath = join(tempDir, 'bundle.js');
await esbuild.build({
entryPoints: [entryPath],
bundle: true,
outfile: outputPath,
format: 'iife',
globalName: 'ComponentBundle',
platform: 'browser',
target: 'es2020',
jsx: 'transform',
jsxFactory: 'React.createElement',
jsxFragment: 'React.Fragment',
alias: {
'react': reactShimPath,
'react-dom': reactShimPath,
'react-dom/client': reactShimPath
},
define: {
'process.env.NODE_ENV': '"production"'
},
loader: {
'.tsx': 'tsx',
'.ts': 'ts',
'.jsx': 'jsx',
'.js': 'js'
},
logLevel: 'error'
});
return readFileSync(outputPath, 'utf-8');
}
/**
* Prepare files for rendering a component
*/
export async function prepareComponentRender(
componentPath: string,
options: RenderOptions = {}
): Promise<{ htmlPath: string; cleanup: () => void }> {
const opts = { ...DEFAULT_OPTIONS, ...options };
const tempDir = join(tmpdir(), `mcp-visual-${randomUUID()}`);
mkdirSync(tempDir, { recursive: true });
try {
const bundledCode = await bundleComponent(componentPath, tempDir);
const html = createComponentHTML(bundledCode, opts);
const htmlPath = join(tempDir, 'index.html');
writeFileSync(htmlPath, html);
return {
htmlPath,
cleanup: () => {
if (existsSync(tempDir)) {
rmSync(tempDir, { recursive: true, force: true });
}
}
};
} catch (error) {
rmSync(tempDir, { recursive: true, force: true });
throw error;
}
}