├── .gitignore ├── .npmignore ├── src ├── utils │ ├── babel-transformer.ts │ └── index.ts ├── components │ └── ErrorBoundary.tsx └── index.tsx ├── tsconfig.json ├── rollup.config.mjs ├── package.json └── Readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | examples/ 3 | .github/ 4 | .gitignore 5 | tsconfig.json -------------------------------------------------------------------------------- /src/utils/babel-transformer.ts: -------------------------------------------------------------------------------- 1 | import * as Babel from "@babel/standalone"; 2 | 3 | export const transform = (code: string, options = {}) => { 4 | return Babel.transform(code, { 5 | presets: ["env", "react"], 6 | ...options, 7 | }); 8 | }; 9 | 10 | export const transformAsync = async (code: string, options = {}) => { 11 | return (Babel as any).transformAsync(code, { 12 | presets: ["env", "react"], 13 | ...options, 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "esnext", 5 | "lib": ["dom", "esnext"], 6 | "jsx": "react", 7 | "declaration": true, 8 | "declarationDir": "dist", 9 | "strict": true, 10 | "moduleResolution": "node", 11 | "allowSyntheticDefaultImports": true, 12 | "esModuleInterop": true, 13 | "skipLibCheck": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "outDir": "dist" 16 | }, 17 | "include": ["src"], 18 | "exclude": ["node_modules", "dist"] 19 | } 20 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import resolve from "@rollup/plugin-node-resolve"; 2 | import commonjs from "@rollup/plugin-commonjs"; 3 | import typescript from "@rollup/plugin-typescript"; 4 | import peerDepsExternal from "rollup-plugin-peer-deps-external"; 5 | import terser from "@rollup/plugin-terser"; 6 | 7 | const processPolyfill = ` 8 | const process = { 9 | env: { 10 | NODE_ENV: 'production' 11 | } 12 | }; 13 | `; 14 | 15 | export default { 16 | input: "src/index.tsx", 17 | output: [ 18 | { 19 | file: "dist/index.esm.js", 20 | format: "esm", 21 | sourcemap: false, 22 | exports: "named", 23 | interop: "auto", 24 | banner: processPolyfill 25 | }, 26 | { 27 | file: "dist/index.js", 28 | format: "cjs", 29 | sourcemap: false, 30 | exports: "named", 31 | interop: "auto", 32 | banner: processPolyfill 33 | } 34 | ], 35 | plugins: [ 36 | peerDepsExternal(), 37 | resolve({ 38 | browser: true, 39 | preferBuiltins: false, 40 | }), 41 | commonjs({ 42 | requireReturnsDefault: "auto", 43 | dynamicRequireTargets: ["node_modules/@babel/standalone/**/*.js"], 44 | transformMixedEsModules: true 45 | }), 46 | typescript({ 47 | tsconfig: "./tsconfig.json", 48 | declaration: true, 49 | declarationDir: "dist", 50 | }), 51 | terser(), 52 | ], 53 | external: ["react", "react-dom"] 54 | }; 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-exe", 3 | "version": "1.0.15", 4 | "description": "A powerful React component executor that renders code with external dependencies and custom styling", 5 | "main": "./dist/index.js", 6 | "module": "./dist/index.esm.js", 7 | "types": "./dist/index.d.ts", 8 | "exports": { 9 | ".": { 10 | "import": "./dist/index.esm.js", 11 | "require": "./dist/index.js", 12 | "types": "./dist/index.d.ts" 13 | } 14 | }, 15 | "browser": { 16 | "process": false 17 | }, 18 | "author": "Vikrant", 19 | "license": "MIT", 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/vgulerianb/react-exe" 23 | }, 24 | "files": [ 25 | "dist" 26 | ], 27 | "keywords": [ 28 | "react", 29 | "code-executor", 30 | "live-preview", 31 | "component-renderer", 32 | "code-playground", 33 | "react-playground", 34 | "typescript", 35 | "tailwind" 36 | ], 37 | "scripts": { 38 | "clean": "rimraf dist", 39 | "build": "npm run clean && rollup -c", 40 | "prepare": "npm run build" 41 | }, 42 | "peerDependencies": { 43 | "react": "^18.0.0", 44 | "react-dom": "^18.0.0" 45 | }, 46 | "dependencies": { 47 | "clsx": "^2.1.0", 48 | "tailwind-merge": "^2.1.0" 49 | }, 50 | "devDependencies": { 51 | "@rollup/plugin-commonjs": "^25.0.0", 52 | "@rollup/plugin-node-resolve": "^15.0.0", 53 | "@rollup/plugin-terser": "^0.4.4", 54 | "@rollup/plugin-typescript": "^11.0.0", 55 | "@types/babel__standalone": "^7.1.7", 56 | "@types/react": "^18.2.48", 57 | "rimraf": "^5.0.0", 58 | "rollup": "^4.9.0", 59 | "rollup-plugin-peer-deps-external": "^2.2.4", 60 | "typescript": "^5.3.3", 61 | "@babel/core": "^7.26.7", 62 | "@babel/standalone": "^7.26.7", 63 | "@babel/preset-env": "^7.26.7", 64 | "@babel/preset-react": "^7.26.3" 65 | } 66 | } -------------------------------------------------------------------------------- /src/components/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface ErrorBoundaryProps { 4 | children: React.ReactNode; 5 | onError?: (error: Error, errorInfo: React.ErrorInfo) => void; 6 | fallback?: React.ReactNode; 7 | } 8 | 9 | interface ErrorBoundaryState { 10 | hasError: boolean; 11 | error: Error | null; 12 | } 13 | 14 | class ErrorBoundary extends React.Component< 15 | ErrorBoundaryProps, 16 | ErrorBoundaryState 17 | > { 18 | constructor(props: ErrorBoundaryProps) { 19 | super(props); 20 | this.state = { 21 | hasError: false, 22 | error: null, 23 | }; 24 | } 25 | 26 | static getDerivedStateFromError(error: Error): ErrorBoundaryState { 27 | return { 28 | hasError: true, 29 | error, 30 | }; 31 | } 32 | 33 | componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { 34 | // Log the error to console 35 | console.error("Error caught by ErrorBoundary:", error, errorInfo); 36 | 37 | // Call the onError callback if provided 38 | if (this.props.onError) { 39 | this.props.onError(error, errorInfo); 40 | } 41 | } 42 | 43 | render() { 44 | if (this.state.hasError) { 45 | // Use provided fallback or default error UI 46 | if (this.props.fallback) { 47 | return this.props.fallback; 48 | } 49 | 50 | return ( 51 |
61 |

Something went wrong

62 |
63 | 64 | Error details 65 | 66 |

67 | {this.state.error?.message || "Unknown error"} 68 |

69 |
78 |               {this.state.error?.stack}
79 |             
80 |
81 |
82 | ); 83 | } 84 | 85 | return this.props.children; 86 | } 87 | } 88 | 89 | export default ErrorBoundary; 90 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | useState, 3 | useCallback, 4 | useEffect, 5 | useMemo, 6 | useRef, 7 | } from "react"; 8 | import { cn, transformMultipleFiles } from "./utils"; 9 | import ErrorBoundary from "./components/ErrorBoundary"; 10 | 11 | const defaultSecurityPatterns = [ 12 | /document\.cookie/i, 13 | /window\.document\.cookie/i, 14 | /eval\(/i, 15 | /Function\(/i, 16 | /document\.write/i, 17 | /document\.location/i, 18 | ]; 19 | 20 | export interface CodeFile { 21 | name: string; 22 | content: string; 23 | isEntry?: boolean; 24 | } 25 | 26 | export interface CodeExecutorConfig { 27 | dependencies?: Record; 28 | containerClassName?: string; 29 | containerStyle?: React.CSSProperties; 30 | errorClassName?: string; 31 | errorStyle?: React.CSSProperties; 32 | securityPatterns?: RegExp[]; 33 | onError?: (error: Error) => void; 34 | enableTailwind?: boolean; 35 | } 36 | 37 | export interface CodeExecutorProps { 38 | code: string | CodeFile[]; 39 | config?: CodeExecutorConfig; 40 | } 41 | 42 | interface ExecutionResult { 43 | Component: React.ComponentType | null; 44 | error: string | null; 45 | forbiddenPatterns: boolean; 46 | } 47 | 48 | const initialExecutionResult: ExecutionResult = { 49 | Component: null, 50 | error: null, 51 | forbiddenPatterns: false, 52 | }; 53 | 54 | // Helper function to compare code content 55 | const isCodeDifferent = ( 56 | prevCode: string | CodeFile[] | undefined, 57 | newCode: string | CodeFile[] 58 | ): boolean => { 59 | if (!prevCode) return true; 60 | 61 | if (typeof prevCode === "string" && typeof newCode === "string") { 62 | return prevCode !== newCode; 63 | } 64 | 65 | if (Array.isArray(prevCode) && Array.isArray(newCode)) { 66 | if (prevCode.length !== newCode.length) return true; 67 | 68 | return prevCode.some((file, index) => { 69 | const newFile = newCode[index]; 70 | return ( 71 | file.name !== newFile.name || 72 | file.content !== newFile.content || 73 | file.isEntry !== newFile.isEntry 74 | ); 75 | }); 76 | } 77 | 78 | return true; 79 | }; 80 | 81 | // Helper function to compare dependencies 82 | const isDependenciesDifferent = ( 83 | prevDeps: Record = {}, 84 | newDeps: Record = {} 85 | ): boolean => { 86 | const prevKeys = Object.keys(prevDeps); 87 | const newKeys = Object.keys(newDeps); 88 | 89 | if (prevKeys.length !== newKeys.length) return true; 90 | 91 | return prevKeys.some((key) => { 92 | const prevValue = prevDeps[key]; 93 | const newValue = newDeps[key]; 94 | 95 | // Compare only the reference for functions and objects 96 | if (typeof prevValue === "function" || typeof newValue === "function") { 97 | return prevValue !== newValue; 98 | } 99 | 100 | // For primitive values, compare the value 101 | return prevValue !== newValue; 102 | }); 103 | }; 104 | 105 | function executeCode( 106 | code: string | CodeFile[], 107 | dependencies: Record, 108 | securityPatterns: RegExp[], 109 | bypassSecurity: boolean 110 | ): ExecutionResult { 111 | try { 112 | const codeFiles = Array.isArray(code) 113 | ? code 114 | : [{ name: "index.tsx", content: code, isEntry: true }]; 115 | 116 | // Security check 117 | if (!bypassSecurity) { 118 | for (const file of codeFiles) { 119 | for (const pattern of securityPatterns) { 120 | if (pattern.test(file.content)) { 121 | return { 122 | Component: null, 123 | error: `Forbidden code pattern detected in ${file.name}: ${pattern}`, 124 | forbiddenPatterns: true, 125 | }; 126 | } 127 | } 128 | } 129 | } 130 | 131 | // Transform the code using our new system 132 | const transformedCode = transformMultipleFiles(codeFiles, dependencies); 133 | 134 | // For debugging 135 | // console.log("Transformed code:", transformedCode); 136 | 137 | // Create the factory function and execute it 138 | const factoryFunction = new Function(transformedCode)(); 139 | const Component = factoryFunction(React, dependencies); 140 | 141 | return { 142 | Component, 143 | error: null, 144 | forbiddenPatterns: false, 145 | }; 146 | } catch (err) { 147 | console.error("Error executing code:", err); 148 | return { 149 | Component: null, 150 | error: err instanceof Error ? err.message : "An unknown error occurred", 151 | forbiddenPatterns: false, 152 | }; 153 | } 154 | } 155 | 156 | export const CodeExecutor: React.FC = ({ 157 | code, 158 | config = {}, 159 | }) => { 160 | const { 161 | dependencies = {}, 162 | containerClassName, 163 | containerStyle, 164 | errorClassName, 165 | errorStyle, 166 | securityPatterns = defaultSecurityPatterns, 167 | onError, 168 | enableTailwind = false, 169 | } = config; 170 | 171 | const [executionResult, setExecutionResult] = useState( 172 | initialExecutionResult 173 | ); 174 | 175 | const { Component, error, forbiddenPatterns } = executionResult; 176 | const prevCodeRef = useRef(); 177 | const prevDependenciesRef = useRef>(); 178 | 179 | // Check if code or dependencies have changed 180 | const hasChanges = useMemo(() => { 181 | const codeChanged = isCodeDifferent(prevCodeRef.current, code); 182 | const dependenciesChanged = isDependenciesDifferent( 183 | prevDependenciesRef.current, 184 | dependencies 185 | ); 186 | 187 | return codeChanged || dependenciesChanged; 188 | }, [code, dependencies]); 189 | 190 | useEffect(() => { 191 | if (enableTailwind) { 192 | const link = document.createElement("link"); 193 | link.href = "https://cdn.tailwindcss.com"; 194 | link.rel = "stylesheet"; 195 | document.head.appendChild(link); 196 | 197 | return () => { 198 | document.head.removeChild(link); 199 | }; 200 | } 201 | }, [enableTailwind]); 202 | 203 | // Execute code on changes 204 | useEffect(() => { 205 | if (hasChanges) { 206 | try { 207 | const result = executeCode(code, dependencies, securityPatterns, false); 208 | setExecutionResult(result); 209 | prevCodeRef.current = code; 210 | prevDependenciesRef.current = dependencies; 211 | } catch (err) { 212 | // Handle any synchronous errors during execution 213 | const errorMessage = err instanceof Error ? err.message : String(err); 214 | setExecutionResult({ 215 | Component: null, 216 | error: errorMessage, 217 | forbiddenPatterns: false, 218 | }); 219 | if (onError && err instanceof Error) { 220 | onError(err); 221 | } 222 | } 223 | } 224 | }, [code, dependencies, securityPatterns, hasChanges, onError]); 225 | 226 | const handleBypassSecurity = useCallback(() => { 227 | try { 228 | const result = executeCode(code, dependencies, securityPatterns, true); 229 | setExecutionResult(result); 230 | prevCodeRef.current = code; 231 | prevDependenciesRef.current = dependencies; 232 | } catch (err) { 233 | const errorMessage = err instanceof Error ? err.message : String(err); 234 | setExecutionResult({ 235 | Component: null, 236 | error: errorMessage, 237 | forbiddenPatterns: false, 238 | }); 239 | if (onError && err instanceof Error) { 240 | onError(err); 241 | } 242 | } 243 | }, [code, dependencies, securityPatterns, onError]); 244 | 245 | const handleExecutionError = useCallback( 246 | (error: Error) => { 247 | setExecutionResult((prev) => ({ 248 | ...prev, 249 | error: error.message, 250 | Component: null, 251 | })); 252 | if (onError) { 253 | onError(error); 254 | } 255 | }, 256 | [onError] 257 | ); 258 | 259 | if (error) { 260 | return ( 261 |
272 |

Error:

273 |

{error}

274 | {forbiddenPatterns && ( 275 |
276 | 291 |
292 | )} 293 |
294 | ); 295 | } 296 | 297 | return ( 298 |
302 | 308 | Powered by{" "} 309 | 314 | React-EXE 315 | 316 | 317 | 331 |

Rendering Error:

332 |

333 | The component failed to render. Check the console for more 334 | details. 335 |

336 |
337 | } 338 | > 339 | {Component ? : null} 340 | 341 | 342 | ); 343 | }; 344 | 345 | export default React.memo(CodeExecutor); 346 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { NodePath } from "@babel/core"; 2 | import * as t from "@babel/types"; 3 | import { type ClassValue, clsx } from "clsx"; 4 | import { twMerge } from "tailwind-merge"; 5 | import { transform } from "./babel-transformer"; 6 | import type { CodeFile } from "../index"; 7 | 8 | const moduleCache = new Map(); 9 | 10 | export const transformMultipleFiles = ( 11 | files: CodeFile[], 12 | dependencies: Record 13 | ) => { 14 | moduleCache.clear(); 15 | 16 | // First pass: preprocess files to extract export information 17 | const exportInfo = new Map< 18 | string, 19 | { 20 | hasDefaultExport: boolean; 21 | namedExports: Set; 22 | exportedName: string | null; 23 | } 24 | >(); 25 | 26 | files.forEach((file) => { 27 | const { modifiedInput, exportedName } = removeDefaultExport(file.content); 28 | 29 | // Find named exports - more comprehensive approach 30 | const namedExports = new Set(); 31 | 32 | // Match regular named exports 33 | const exportRegex = 34 | /export\s+(const|let|var|function|class)\s+([A-Za-z0-9_$]+)/g; 35 | let match; 36 | 37 | while ((match = exportRegex.exec(modifiedInput)) !== null) { 38 | namedExports.add(match[2]); 39 | } 40 | 41 | // Match "export { x, y, z }" style exports 42 | const exportBraceRegex = /export\s+{([^}]+)}/g; 43 | while ((match = exportBraceRegex.exec(modifiedInput)) !== null) { 44 | const exportsList = match[1].split(","); 45 | for (const exportItem of exportsList) { 46 | // Handle "originalName as exportName" syntax 47 | const nameParts = exportItem.trim().split(/\s+as\s+/); 48 | const exportName = 49 | nameParts.length > 1 ? nameParts[1].trim() : nameParts[0].trim(); 50 | if (exportName) namedExports.add(exportName); 51 | } 52 | } 53 | 54 | exportInfo.set(file.name, { 55 | hasDefaultExport: exportedName !== null, 56 | namedExports, 57 | exportedName, 58 | }); 59 | }); 60 | 61 | // Transform all files 62 | files.forEach((file) => { 63 | const fileExportInfo = exportInfo.get(file.name); 64 | const { modifiedInput, exportedName } = removeDefaultExport(file.content); 65 | 66 | const dependencyVarMap = new Map(); 67 | Object.keys(dependencies).forEach((dep) => { 68 | const safeName = dep.replace(/[^a-zA-Z0-9_]/g, "_"); 69 | dependencyVarMap.set(dep, safeName); 70 | }); 71 | 72 | // Pre-process to handle various exports 73 | let processedInput = modifiedInput; 74 | 75 | // Replace "export const/function" with plain declarations 76 | processedInput = processedInput.replace( 77 | /export\s+(const|let|var|function|class)\s+([A-Za-z0-9_$]+)/g, 78 | "$1 $2" 79 | ); 80 | 81 | // Handle "export { x, y, z }" syntax - remove these lines 82 | processedInput = processedInput.replace(/export\s+{[^}]+};?/g, ""); 83 | 84 | // Remove type exports 85 | processedInput = processedInput.replace(/export\s+type\s+[^;]+;/g, ""); 86 | processedInput = processedInput.replace( 87 | /export\s+interface\s+[^{]+{[^}]+}/g, 88 | "" 89 | ); 90 | 91 | // Remove React imports since we're injecting React globally 92 | if ( 93 | processedInput.includes("import React from") || 94 | processedInput.includes("import * as React from") 95 | ) { 96 | processedInput = processedInput.replace( 97 | /import\s+(\*\s+as\s+)?React\s+from\s+['"]react['"];?/g, 98 | "" 99 | ); 100 | } 101 | 102 | const transpiledCode = transform(processedInput, { 103 | presets: [ 104 | ["typescript", { isTSX: true, allExtensions: true }], 105 | ["react"], 106 | ], 107 | plugins: [ 108 | createImportTransformerPlugin( 109 | Object.keys(dependencies), 110 | dependencyVarMap, 111 | files, 112 | exportInfo 113 | ), 114 | ], 115 | }).code; 116 | 117 | // Store both the transpiled code and export information 118 | moduleCache.set(file.name, { 119 | code: transpiledCode, 120 | exportedName, 121 | exportInfo: fileExportInfo, 122 | } as any); 123 | }); 124 | 125 | const entryFile = files.find((f) => f.isEntry) || files[0]; 126 | const entryModule = moduleCache.get(entryFile.name); 127 | 128 | if (!entryModule) { 129 | throw new Error("Entry module not found"); 130 | } 131 | 132 | const dependencyVars = Object.keys(dependencies) 133 | .map((dep) => { 134 | const safeName = dep.replace(/[^a-zA-Z0-9_]/g, "_"); 135 | return `const ${safeName} = dependencies['${dep}'];`; 136 | }) 137 | .join("\n "); 138 | 139 | // Create the module registry 140 | const moduleRegistryCode = ` 141 | const moduleCache = new Map(); 142 | const moduleDefinitions = new Map(); 143 | `; 144 | 145 | // Create module definitions with improved exports handling 146 | const moduleDefinitions = Array.from(moduleCache.entries()) 147 | .map(([name, module]: [string, any]) => { 148 | const normalizedName = normalizeFilename(name); 149 | 150 | // Get export info 151 | const info = module.exportInfo || { 152 | hasDefaultExport: module.exportedName !== null, 153 | namedExports: new Set(), 154 | exportedName: module.exportedName, 155 | }; 156 | 157 | // Extract all named exports directly from the code 158 | const namedExports = new Set(); 159 | 160 | // Check for export statements like "export const useCounter" 161 | const exportConstRegex = 162 | /export\s+(const|let|var|function|class)\s+([A-Za-z0-9_$]+)/g; 163 | 164 | let hookName = null; 165 | 166 | // Look for direct named exports in the original code 167 | const originalCode = files.find((f) => f.name === name)?.content || ""; 168 | let exportMatch; 169 | while ((exportMatch = exportConstRegex.exec(originalCode)) !== null) { 170 | namedExports.add(exportMatch[2]); 171 | if (name.includes("use")) { 172 | hookName = exportMatch[2]; 173 | // console.log("Found named export:", exportMatch[2]); 174 | } 175 | } 176 | 177 | // Prepare exports handling code 178 | let exportsSetup = ""; 179 | 180 | // For default exports 181 | if (info.hasDefaultExport && info.exportedName) { 182 | exportsSetup += ` 183 | // Handle default export 184 | exports.default = ${info.exportedName}; 185 | // For CommonJS compatibility 186 | module.exports = Object.assign({}, module.exports, typeof ${info.exportedName} === 'function' 187 | ? { default: ${info.exportedName} } 188 | : ${info.exportedName}); 189 | `; 190 | } 191 | 192 | // Explicitly handle named exports we found 193 | for (const exportName of Array.from(namedExports)) { 194 | exportsSetup += ` 195 | // Handle named export: ${exportName} 196 | if (typeof ${exportName} !== 'undefined') { 197 | exports.${exportName} = ${exportName}; 198 | } 199 | `; 200 | } 201 | 202 | // For hooks add explicit export handling 203 | if (hookName && name.includes(hookName)) { 204 | exportsSetup += ` 205 | if (typeof ${hookName} !== 'undefined') { 206 | exports.${hookName} = ${hookName}; 207 | } 208 | `; 209 | } 210 | 211 | return ` 212 | moduleDefinitions.set("${normalizedName}", function(React) { 213 | const module = { exports: {} }; 214 | const exports = module.exports; 215 | 216 | try { 217 | (function(module, exports) { 218 | ${module.code} 219 | 220 | ${exportsSetup} 221 | })(module, exports); 222 | } catch (error) { 223 | console.error("Error in module ${normalizedName}:", error); 224 | throw error; 225 | } 226 | 227 | return module.exports; 228 | }); 229 | `; 230 | }) 231 | .join("\n\n"); 232 | 233 | // Create module getter with better caching 234 | const moduleGetterCode = ` 235 | function getModule(name) { 236 | if (!moduleCache.has(name)) { 237 | const moduleFactory = moduleDefinitions.get(name); 238 | if (!moduleFactory) { 239 | throw new Error(\`Module "\${name}" not found\`); 240 | } 241 | try { 242 | const moduleExports = moduleFactory(React); 243 | // Ensure we're getting a proper object with exports 244 | if (typeof moduleExports !== 'object' && typeof moduleExports !== 'function') { 245 | throw new Error(\`Module "\${name}" did not return a valid exports object\`); 246 | } 247 | moduleCache.set(name, moduleExports); 248 | } catch (error) { 249 | console.error(\`Error initializing module "\${name}"\`, error); 250 | throw error; 251 | } 252 | } 253 | return moduleCache.get(name); 254 | } 255 | `; 256 | 257 | const entryModuleName = normalizeFilename(entryFile.name); 258 | 259 | return ` 260 | return function(React, dependencies) { 261 | // Verify that React has all the necessary hooks and components 262 | if (!React.useState || !React.useEffect || !React.useMemo || !React.useCallback || !React.useRef) { 263 | console.warn("React object is missing hooks. This may cause issues with hook usage in components."); 264 | } 265 | 266 | ${dependencyVars} 267 | 268 | ${moduleRegistryCode} 269 | 270 | ${moduleDefinitions} 271 | 272 | ${moduleGetterCode} 273 | 274 | try { 275 | const entryModule = getModule("${entryModuleName}"); 276 | // More robust handling of the component export 277 | const Component = entryModule.default || entryModule; 278 | 279 | // Validate that we're returning a valid component 280 | if (typeof Component !== 'function') { 281 | throw new Error(\`Expected a React component but got \${typeof Component} (\${JSON.stringify(Component)}). Check that your component is properly exported.\`); 282 | } 283 | 284 | return Component; 285 | } catch (err) { 286 | console.error("Error loading component:", err); 287 | // Return a fallback component that displays the error 288 | return function ErrorComponent() { 289 | return React.createElement('div', { 290 | style: { 291 | color: 'red', 292 | padding: '1rem', 293 | border: '1px solid red', 294 | borderRadius: '0.25rem' 295 | } 296 | }, [ 297 | React.createElement('h3', { key: 'title' }, 'Error Loading Component'), 298 | React.createElement('pre', { key: 'error' }, String(err.message || err)), 299 | React.createElement('div', { key: 'stack', style: { marginTop: '1rem' } }, 300 | React.createElement('details', {}, [ 301 | React.createElement('summary', { key: 'summary' }, 'Stack Trace'), 302 | React.createElement('pre', { key: 'trace', style: { fontSize: '0.8rem', whiteSpace: 'pre-wrap' } }, err.stack || 'No stack trace available') 303 | ]) 304 | ) 305 | ]); 306 | }; 307 | } 308 | } 309 | `; 310 | }; 311 | 312 | const normalizeFilename = (filename: string) => { 313 | // Remove all file extensions (.js, .jsx, .ts, .tsx) 314 | return filename.replace(/\.(js|jsx|ts|tsx)$/, "").replace(/^\.\//, ""); 315 | }; 316 | 317 | const createImportTransformerPlugin = ( 318 | allowedDependencies: string[], 319 | dependencyVarMap: Map, 320 | localModules: CodeFile[], 321 | exportInfo: Map< 322 | string, 323 | { 324 | hasDefaultExport: boolean; 325 | namedExports: Set; 326 | exportedName: string | null; 327 | } 328 | > = new Map() 329 | ) => { 330 | // Normalize paths for easier lookup 331 | const normalizedModulePaths = new Map(); 332 | 333 | localModules.forEach((module) => { 334 | const normalizedPath = normalizeFilename(module.name); 335 | normalizedModulePaths.set(normalizedPath, module.name); 336 | }); 337 | 338 | return () => ({ 339 | name: "import-transformer", 340 | visitor: { 341 | ImportDeclaration(path: NodePath) { 342 | const source = path.node.source.value; 343 | const specifiers = path.node.specifiers; 344 | 345 | if (specifiers.length === 0) return; 346 | 347 | // Special case for React imports 348 | if (source === "react") { 349 | const newNodes: t.Statement[] = []; 350 | 351 | // Process each React import specifier 352 | specifiers.forEach((specifier) => { 353 | // Skip the default import (React itself) as it's already available 354 | if (t.isImportDefaultSpecifier(specifier)) { 355 | // No need to do anything as React is already in scope 356 | } else if (t.isImportSpecifier(specifier)) { 357 | // For named imports like useState, useEffect, etc. 358 | const imported = specifier.imported; 359 | const importedName = t.isIdentifier(imported) 360 | ? imported.name 361 | : t.isStringLiteral(imported) 362 | ? imported.value 363 | : null; 364 | 365 | if (importedName !== null) { 366 | // Create a variable declaration to pull the named export from React 367 | newNodes.push( 368 | t.variableDeclaration("const", [ 369 | t.variableDeclarator( 370 | t.identifier(specifier.local.name), 371 | t.memberExpression( 372 | t.identifier("React"), 373 | t.identifier(importedName) 374 | ) 375 | ), 376 | ]) 377 | ); 378 | } 379 | } 380 | }); 381 | 382 | // Replace the import declaration with our new variable declarations 383 | if (newNodes.length > 0) { 384 | path.replaceWithMultiple(newNodes); 385 | } else { 386 | path.remove(); 387 | } 388 | return; 389 | } 390 | 391 | const normalizedSource = normalizeFilename(source); 392 | const isLocalModule = normalizedModulePaths.has(normalizedSource); 393 | 394 | if ( 395 | !isLocalModule && 396 | !allowedDependencies.includes(source) && 397 | source !== "react" 398 | ) { 399 | throw new Error(`Module not found: ${source}`); 400 | } 401 | 402 | let newNodes: t.Statement[] = []; 403 | 404 | if (isLocalModule) { 405 | const originalModuleName = 406 | normalizedModulePaths.get(normalizedSource) || ""; 407 | const moduleExportInfo = exportInfo.get(originalModuleName); 408 | 409 | specifiers.forEach((specifier) => { 410 | if (t.isImportDefaultSpecifier(specifier)) { 411 | // For default imports, get the module and use its default export 412 | newNodes.push( 413 | t.variableDeclaration("const", [ 414 | t.variableDeclarator( 415 | t.identifier(specifier.local.name), 416 | t.memberExpression( 417 | t.callExpression(t.identifier("getModule"), [ 418 | t.stringLiteral(normalizedSource), 419 | ]), 420 | t.identifier("default") 421 | ) 422 | ), 423 | ]) 424 | ); 425 | } else if (t.isImportSpecifier(specifier)) { 426 | const imported = specifier.imported; 427 | const importedName = t.isIdentifier(imported) 428 | ? imported.name 429 | : t.isStringLiteral(imported) 430 | ? imported.value 431 | : null; 432 | 433 | if (importedName !== null) { 434 | // Check if this is a named export from the module 435 | const isNamedExport = 436 | moduleExportInfo && 437 | moduleExportInfo.namedExports.has(importedName); 438 | 439 | // Create appropriate access to the module export 440 | newNodes.push( 441 | t.variableDeclaration("const", [ 442 | t.variableDeclarator( 443 | t.identifier(specifier.local.name), 444 | t.memberExpression( 445 | t.callExpression(t.identifier("getModule"), [ 446 | t.stringLiteral(normalizedSource), 447 | ]), 448 | t.identifier(importedName) 449 | ) 450 | ), 451 | ]) 452 | ); 453 | 454 | // Add debug comment for easier troubleshooting 455 | if (!isNamedExport) { 456 | console.warn( 457 | `Warning: Importing '${importedName}' from '${source}' but it may not be exported` 458 | ); 459 | } 460 | } 461 | } 462 | }); 463 | } else { 464 | const sourceVarName = dependencyVarMap.get(source) || source; 465 | 466 | specifiers.forEach((specifier) => { 467 | if (t.isImportDefaultSpecifier(specifier)) { 468 | newNodes.push( 469 | t.variableDeclaration("const", [ 470 | t.variableDeclarator( 471 | t.identifier(specifier.local.name), 472 | t.identifier(sourceVarName) 473 | ), 474 | ]) 475 | ); 476 | } else if (t.isImportSpecifier(specifier)) { 477 | const imported = specifier.imported; 478 | const importedName = t.isIdentifier(imported) 479 | ? imported.name 480 | : t.isStringLiteral(imported) 481 | ? imported.value 482 | : null; 483 | 484 | if (importedName !== null) { 485 | newNodes.push( 486 | t.variableDeclaration("const", [ 487 | t.variableDeclarator( 488 | t.identifier(specifier.local.name), 489 | t.memberExpression( 490 | t.identifier(sourceVarName), 491 | t.identifier(importedName) 492 | ) 493 | ), 494 | ]) 495 | ); 496 | } 497 | } 498 | }); 499 | } 500 | 501 | path.replaceWithMultiple(newNodes); 502 | }, 503 | 504 | // Handle TypeScript import types (remove them) 505 | TSImportType(path: { remove: () => void }) { 506 | path.remove(); 507 | }, 508 | 509 | // Handle TypeScript export declarations 510 | ExportNamedDeclaration(path: { 511 | node: { declaration: any; specifiers: string | any[] }; 512 | replaceWith: (arg0: any) => void; 513 | remove: () => void; 514 | }) { 515 | // For named exports, we need to keep the declaration but remove the export 516 | const declaration = path.node.declaration; 517 | 518 | if (declaration) { 519 | // Replace the export declaration with just the declaration 520 | path.replaceWith(declaration); 521 | } else if (path.node.specifiers.length > 0) { 522 | // For export { name } from 'module' style exports 523 | path.remove(); 524 | } 525 | }, 526 | 527 | ExportDefaultDeclaration(path: { 528 | node: { declaration: any }; 529 | remove: () => void; 530 | replaceWith: (arg0: t.FunctionDeclaration) => void; 531 | }) { 532 | const declaration = path.node.declaration; 533 | 534 | if (t.isIdentifier(declaration)) { 535 | // For: export default ComponentName; 536 | path.remove(); 537 | } else if (t.isFunctionDeclaration(declaration) && declaration.id) { 538 | // For: export default function ComponentName() {} 539 | path.replaceWith(declaration); 540 | } else { 541 | // For anonymous declarations: export default function() {} 542 | // Convert to a variable declaration 543 | path.remove(); 544 | } 545 | }, 546 | 547 | // Remove all type-only exports 548 | TSTypeAliasDeclaration(path: { 549 | parent: t.Node | null | undefined; 550 | parentPath: { remove: () => void }; 551 | }) { 552 | if (path.parent && t.isExportNamedDeclaration(path.parent)) { 553 | path.parentPath.remove(); 554 | } 555 | }, 556 | 557 | TSInterfaceDeclaration(path: { 558 | parent: t.Node | null | undefined; 559 | parentPath: { remove: () => void }; 560 | }) { 561 | if (path.parent && t.isExportNamedDeclaration(path.parent)) { 562 | path.parentPath.remove(); 563 | } 564 | }, 565 | }, 566 | }); 567 | }; 568 | 569 | export const removeDefaultExport = ( 570 | input: string 571 | ): { modifiedInput: string; exportedName: string | null } => { 572 | const defaultExportWithDeclarationRegex = 573 | /export\s+default\s+(?:async\s+)?function\s+([A-Za-z0-9_]+)\s*(?:<[^>]*>)?\s*\([^)]*\)\s*(?::\s*[^{]*\s*)?\s*{[^}]*}/; 574 | const defaultExportRegex = /export\s+default\s+([A-Za-z0-9_]+)(?:<[^>]*>)?;?/; 575 | const typeExportRegex = /export\s+type\s+[^;]+;/g; 576 | const interfaceExportRegex = /export\s+interface\s+[^{]+{[^}]+}/g; 577 | 578 | let match = input.match(defaultExportWithDeclarationRegex); 579 | let exportedName: string | null = null; 580 | let modifiedInput = input 581 | .replace(typeExportRegex, "") 582 | .replace(interfaceExportRegex, ""); 583 | 584 | if (match) { 585 | exportedName = match[1]; 586 | modifiedInput = modifiedInput 587 | .replace(/export\s+default\s+(?:async\s+)?function/, "function") 588 | .trim(); 589 | } else { 590 | match = input.match(defaultExportRegex); 591 | if (match) { 592 | exportedName = match[1]; 593 | modifiedInput = modifiedInput.replace(defaultExportRegex, "").trim(); 594 | } 595 | } 596 | 597 | return { modifiedInput, exportedName }; 598 | }; 599 | 600 | export function cn(...inputs: ClassValue[]) { 601 | return twMerge(clsx(inputs)); 602 | } 603 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # React-EXE 2 | 3 | Execute React components on the fly with external dependencies, custom styling, and TypeScript support. Perfect for creating live code previews, documentation, or interactive code playgrounds. 4 | 5 | Screenshot 2025-02-26 at 00 23 34 6 | 7 | Try the live demo [here](https://react-exe-demo.vercel.app/). 8 | 9 | ## Features 10 | 11 | - 🚀 Execute React components from string code 12 | - 📦 Support for external dependencies 13 | - 🎨 Tailwind CSS support 14 | - 🔒 Built-in security checks 15 | - 💅 Customizable styling 16 | - 📝 TypeScript support 17 | - ⚡ Live rendering 18 | - 🐛 Error boundary protection 19 | - 📄 Multi-file support 20 | 21 | ## Installation 22 | 23 | ```bash 24 | npm install react-exe 25 | # or 26 | yarn add react-exe 27 | # or 28 | pnpm add react-exe 29 | ``` 30 | 31 | ## Vite Configuration 32 | 33 | If you're using Vite, you need to add the following configuration to your `vite.config.js` or `vite.config.ts`: 34 | 35 | ```js 36 | import { defineConfig } from 'vite' 37 | 38 | export default defineConfig({ 39 | define: { 40 | 'process.env': {} 41 | } 42 | // ... rest of your config 43 | }) 44 | ``` 45 | 46 | This is required to ensure proper functionality in Vite projects. 47 | 48 | ## Basic Usage 49 | 50 | ```tsx 51 | import { CodeExecutor } from "react-exe"; 52 | 53 | const code = ` 54 | export default function HelloWorld() { 55 | return ( 56 |
57 |

Hello World!

58 |
59 | ); 60 | } 61 | `; 62 | 63 | function App() { 64 | return ; 65 | } 66 | ``` 67 | 68 | ## Advanced Usage 69 | 70 | ### With External Dependencies 71 | 72 | ```tsx 73 | import { CodeExecutor } from "react-exe"; 74 | import * as echarts from "echarts"; 75 | import * as framerMotion from "framer-motion"; 76 | 77 | const code = ` 78 | import { motion } from 'framer-motion'; 79 | import { LineChart } from 'echarts'; 80 | 81 | export default function Dashboard() { 82 | return ( 83 | 88 | 96 | 97 | ); 98 | } 99 | `; 100 | 101 | function App() { 102 | return ( 103 | 118 | ); 119 | } 120 | ``` 121 | 122 | ### With absolute imports and wildcard patterns 123 | 124 | ```tsx 125 | import { CodeExecutor } from "react-exe"; 126 | import * as echarts from "echarts"; 127 | import * as framerMotion from "framer-motion"; 128 | import * as uiComponents from "../ShadcnComps"; 129 | 130 | const code = ` 131 | import { motion } from 'framer-motion'; 132 | import { LineChart } from 'echarts'; 133 | import { Button } from "@/components/ui/button" 134 | 135 | export default function Dashboard() { 136 | return ( 137 | 142 | 150 | 151 | ); 152 | } 153 | `; 154 | 155 | function App() { 156 | return ( 157 | 173 | ); 174 | } 175 | ``` 176 | 177 | ### With Multiple Files 178 | 179 | React-EXE supports multiple files with cross-imports, allowing you to build more complex components and applications: 180 | 181 | ```tsx 182 | import { CodeExecutor } from "react-exe"; 183 | import * as framerMotion from "framer-motion"; 184 | 185 | // Define multiple files as an array of code files 186 | const files = [ 187 | { 188 | name: "App.tsx", // Main entry file 189 | content: ` 190 | import React from 'react'; 191 | import { motion } from 'framer-motion'; 192 | import Header from './Header'; 193 | import Counter from './Counter'; 194 | 195 | const App = () => { 196 | return ( 197 | 202 |
203 | 204 | 205 | ); 206 | }; 207 | 208 | export default App; 209 | `, 210 | isEntry: true, // Mark this as the entry point 211 | }, 212 | { 213 | name: "Header.tsx", 214 | content: ` 215 | import React from 'react'; 216 | 217 | interface HeaderProps { 218 | title: string; 219 | } 220 | 221 | const Header = ({ title }: HeaderProps) => { 222 | return ( 223 |
224 |

{title}

225 |
226 | ); 227 | }; 228 | 229 | export default Header; 230 | `, 231 | }, 232 | { 233 | name: "Counter.tsx", 234 | content: ` 235 | import React, { useState } from 'react'; 236 | import { motion } from 'framer-motion'; 237 | import CounterButton from './CounterButton'; 238 | 239 | const Counter = () => { 240 | const [count, setCount] = useState(0); 241 | 242 | const increment = () => setCount(prev => prev + 1); 243 | const decrement = () => setCount(prev => prev - 1); 244 | 245 | return ( 246 |
247 |

Counter Component

248 | 249 | 255 | {count} 256 | 257 | 258 |
259 | 260 | 261 |
262 |
263 | ); 264 | }; 265 | 266 | export default Counter; 267 | `, 268 | }, 269 | { 270 | name: "CounterButton.tsx", 271 | content: ` 272 | import React from 'react'; 273 | import { motion } from 'framer-motion'; 274 | 275 | interface CounterButtonProps { 276 | onClick: () => void; 277 | label: string; 278 | variant?: 'primary' | 'success' | 'danger'; 279 | } 280 | 281 | const CounterButton = ({ 282 | onClick, 283 | label, 284 | variant = 'primary' 285 | }: CounterButtonProps) => { 286 | 287 | const getButtonColor = () => { 288 | switch(variant) { 289 | case 'success': return 'bg-green-500 hover:bg-green-600'; 290 | case 'danger': return 'bg-red-500 hover:bg-red-600'; 291 | default: return 'bg-blue-500 hover:bg-blue-600'; 292 | } 293 | }; 294 | 295 | return ( 296 | 302 | {label} 303 | 304 | ); 305 | }; 306 | 307 | export default CounterButton; 308 | `, 309 | }, 310 | ]; 311 | 312 | function App() { 313 | return ( 314 | 324 | ); 325 | } 326 | ``` 327 | 328 | ### Creating a Project Structure with Multiple Files 329 | 330 | For more complex applications, you can organize your files in a project-like structure: 331 | 332 | ```tsx 333 | import { CodeExecutor } from "react-exe"; 334 | import * as reactRouter from "react-router-dom"; 335 | import * as framerMotion from "framer-motion"; 336 | 337 | const files = [ 338 | { 339 | name: "App.tsx", 340 | content: ` 341 | import React from 'react'; 342 | import { BrowserRouter, Routes, Route } from 'react-router-dom'; 343 | import Layout from './components/Layout'; 344 | import Home from './pages/Home'; 345 | import About from './pages/About'; 346 | import NotFound from './pages/NotFound'; 347 | 348 | const App = () => { 349 | return ( 350 | 351 | 352 | }> 353 | } /> 354 | } /> 355 | } /> 356 | 357 | 358 | 359 | ); 360 | }; 361 | 362 | export default App; 363 | `, 364 | isEntry: true, 365 | }, 366 | { 367 | name: "components/Layout.tsx", 368 | content: ` 369 | import React from 'react'; 370 | import { Outlet } from 'react-router-dom'; 371 | import Navbar from './Navbar'; 372 | import Footer from './Footer'; 373 | 374 | const Layout = () => { 375 | return ( 376 |
377 | 378 |
379 | 380 |
381 |
382 |
383 | ); 384 | }; 385 | 386 | export default Layout; 387 | `, 388 | }, 389 | { 390 | name: "components/Navbar.tsx", 391 | content: ` 392 | import React from 'react'; 393 | import { Link, useLocation } from 'react-router-dom'; 394 | 395 | const Navbar = () => { 396 | const location = useLocation(); 397 | 398 | const isActive = (path: string) => { 399 | return location.pathname === path ? 400 | 'text-white bg-indigo-700' : 401 | 'text-indigo-200 hover:text-white hover:bg-indigo-600'; 402 | }; 403 | 404 | return ( 405 | 427 | ); 428 | }; 429 | 430 | export default Navbar; 431 | `, 432 | }, 433 | { 434 | name: "components/Footer.tsx", 435 | content: ` 436 | import React from 'react'; 437 | 438 | const Footer = () => { 439 | return ( 440 |
441 |
442 |

© {new Date().getFullYear()} React-EXE Demo

443 |

Built with multiple files

444 |
445 |
446 | ); 447 | }; 448 | 449 | export default Footer; 450 | `, 451 | }, 452 | { 453 | name: "pages/Home.tsx", 454 | content: ` 455 | import React from 'react'; 456 | import { motion } from 'framer-motion'; 457 | 458 | const Home = () => { 459 | return ( 460 | 465 |

Welcome to the Home Page

466 |

This is a multi-file application example using React-EXE.

467 |

468 | It demonstrates how you can create complex applications with multiple 469 | components, pages, and even routing! 470 |

471 | 472 |
473 |

Features Demonstrated:

474 |
    475 |
  • Multiple file structure
  • 476 |
  • React Router integration
  • 477 |
  • Animation with Framer Motion
  • 478 |
  • Component composition
  • 479 |
  • Styling with Tailwind CSS
  • 480 |
481 |
482 |
483 | ); 484 | }; 485 | 486 | export default Home; 487 | `, 488 | }, 489 | { 490 | name: "pages/About.tsx", 491 | content: ` 492 | import React from 'react'; 493 | import { motion } from 'framer-motion'; 494 | 495 | const About = () => { 496 | return ( 497 | 502 |

About Page

503 |

504 | React-EXE is a powerful library for executing React components on the fly. 505 | It supports multi-file applications like this one! 506 |

507 | 508 | 522 | {[1, 2, 3].map((item) => ( 523 | 531 |

Feature {item}

532 |

533 | This is an example of a card that demonstrates Framer Motion animations 534 | in a multi-file React component. 535 |

536 |
537 | ))} 538 |
539 |
540 | ); 541 | }; 542 | 543 | export default About; 544 | `, 545 | }, 546 | { 547 | name: "pages/NotFound.tsx", 548 | content: ` 549 | import React from 'react'; 550 | import { Link } from 'react-router-dom'; 551 | import { motion } from 'framer-motion'; 552 | 553 | const NotFound = () => { 554 | return ( 555 | 561 | 570 |

404

571 |
572 | 573 |

Page Not Found

574 |

575 | The page you're looking for doesn't exist or has been moved. 576 |

577 | 578 | 582 | Return Home 583 | 584 |
585 | ); 586 | }; 587 | 588 | export default NotFound; 589 | `, 590 | }, 591 | ]; 592 | 593 | function App() { 594 | return ( 595 | 605 | ); 606 | } 607 | ``` 608 | 609 | ### Using Custom Hooks and Utilities in Multi-File Apps 610 | 611 | You can also create and use custom hooks, utilities, and TypeScript types across multiple files: 612 | 613 | ```tsx 614 | import { CodeExecutor } from "react-exe"; 615 | 616 | const files = [ 617 | { 618 | name: "App.tsx", 619 | content: ` 620 | import React from 'react'; 621 | import ThemeProvider from './theme/ThemeProvider'; 622 | import ThemeSwitcher from './components/ThemeSwitcher'; 623 | import UserProfile from './components/UserProfile'; 624 | import { fetchUserData } from './utils/api'; 625 | 626 | const App = () => { 627 | return ( 628 | 629 |
630 |
631 |
632 | 633 |
634 | 635 |
636 |
637 |
638 | ); 639 | }; 640 | 641 | export default App; 642 | `, 643 | isEntry: true, 644 | }, 645 | { 646 | name: "types/index.ts", 647 | content: ` 648 | export interface User { 649 | id: string; 650 | name: string; 651 | email: string; 652 | avatar: string; 653 | } 654 | 655 | export type Theme = 'light' | 'dark' | 'system'; 656 | 657 | export interface ThemeContextType { 658 | theme: Theme; 659 | setTheme: (theme: Theme) => void; 660 | } 661 | `, 662 | }, 663 | { 664 | name: "theme/ThemeProvider.tsx", 665 | content: ` 666 | import React, { createContext, useContext, useState, useEffect } from 'react'; 667 | import { Theme, ThemeContextType } from '../types'; 668 | 669 | const ThemeContext = createContext(undefined); 670 | 671 | const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { 672 | const [theme, setTheme] = useState('system'); 673 | 674 | useEffect(() => { 675 | const applyTheme = (newTheme: Theme) => { 676 | const root = window.document.documentElement; 677 | 678 | // Remove any existing theme classes 679 | root.classList.remove('light', 'dark'); 680 | 681 | // Apply the appropriate theme 682 | if (newTheme === 'system') { 683 | const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; 684 | root.classList.add(systemTheme); 685 | } else { 686 | root.classList.add(newTheme); 687 | } 688 | }; 689 | 690 | applyTheme(theme); 691 | 692 | // Listen for system theme changes 693 | const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); 694 | const handleChange = () => { 695 | if (theme === 'system') { 696 | applyTheme('system'); 697 | } 698 | }; 699 | 700 | mediaQuery.addEventListener('change', handleChange); 701 | return () => mediaQuery.removeEventListener('change', handleChange); 702 | }, [theme]); 703 | 704 | return ( 705 | 706 | {children} 707 | 708 | ); 709 | }; 710 | 711 | export const useTheme = () => { 712 | const context = useContext(ThemeContext); 713 | if (context === undefined) { 714 | throw new Error('useTheme must be used within a ThemeProvider'); 715 | } 716 | return context; 717 | }; 718 | 719 | export default ThemeProvider; 720 | `, 721 | }, 722 | { 723 | name: "components/ThemeSwitcher.tsx", 724 | content: ` 725 | import React from 'react'; 726 | import { useTheme } from '../theme/ThemeProvider'; 727 | import { Theme } from '../types'; 728 | 729 | const ThemeSwitcher = () => { 730 | const { theme, setTheme } = useTheme(); 731 | 732 | const themes: { value: Theme; label: string }[] = [ 733 | { value: 'light', label: '☀️ Light' }, 734 | { value: 'dark', label: '🌙 Dark' }, 735 | { value: 'system', label: '🖥️ System' } 736 | ]; 737 | 738 | return ( 739 |
740 |
741 | {themes.map(({ value, label }) => ( 742 | 753 | ))} 754 |
755 |
756 | ); 757 | }; 758 | 759 | export default ThemeSwitcher; 760 | `, 761 | }, 762 | { 763 | name: "hooks/useUser.ts", 764 | content: ` 765 | import { useState, useEffect } from 'react'; 766 | import { User } from '../types'; 767 | 768 | export const useUser = ( 769 | userId: string, 770 | fetchUserData: (id: string) => Promise 771 | ) => { 772 | const [user, setUser] = useState(null); 773 | const [loading, setLoading] = useState(true); 774 | const [error, setError] = useState(null); 775 | 776 | useEffect(() => { 777 | let isMounted = true; 778 | 779 | const loadUser = async () => { 780 | try { 781 | setLoading(true); 782 | const userData = await fetchUserData(userId); 783 | 784 | if (isMounted) { 785 | setUser(userData); 786 | setError(null); 787 | } 788 | } catch (err) { 789 | if (isMounted) { 790 | setError('Failed to load user'); 791 | setUser(null); 792 | } 793 | } finally { 794 | if (isMounted) { 795 | setLoading(false); 796 | } 797 | } 798 | }; 799 | 800 | loadUser(); 801 | 802 | return () => { 803 | isMounted = false; 804 | }; 805 | }, [userId, fetchUserData]); 806 | 807 | return { user, loading, error }; 808 | }; 809 | `, 810 | }, 811 | { 812 | name: "utils/api.ts", 813 | content: ` 814 | import { User } from '../types'; 815 | 816 | // Simulate API call with mock data 817 | export const fetchUserData = async (userId: string): Promise => { 818 | // Simulate network delay 819 | await new Promise(resolve => setTimeout(resolve, 1000)); 820 | 821 | // Mock data 822 | const users: Record = { 823 | '1': { 824 | id: '1', 825 | name: 'John Doe', 826 | email: 'john@example.com', 827 | avatar: 'https://randomuser.me/api/portraits/men/32.jpg' 828 | }, 829 | '2': { 830 | id: '2', 831 | name: 'Jane Smith', 832 | email: 'jane@example.com', 833 | avatar: 'https://randomuser.me/api/portraits/women/44.jpg' 834 | } 835 | }; 836 | 837 | const user = users[userId]; 838 | 839 | if (!user) { 840 | throw new Error(\`User with ID \${userId} not found\`); 841 | } 842 | 843 | return user; 844 | }; 845 | `, 846 | }, 847 | { 848 | name: "components/UserProfile.tsx", 849 | content: ` 850 | import React from 'react'; 851 | import { useUser } from '../hooks/useUser'; 852 | import { User } from '../types'; 853 | 854 | interface UserProfileProps { 855 | userId: string; 856 | fetchUserData: (id: string) => Promise; 857 | } 858 | 859 | const UserProfile = ({ userId, fetchUserData }: UserProfileProps) => { 860 | const { user, loading, error } = useUser(userId, fetchUserData); 861 | 862 | if (loading) { 863 | return ( 864 |
865 |
866 |
867 |
868 |
869 |
870 |
871 |
872 |
873 | ); 874 | } 875 | 876 | if (error) { 877 | return ( 878 |
879 |

{error}

880 |
881 | ); 882 | } 883 | 884 | if (!user) { 885 | return
No user found
; 886 | } 887 | 888 | return ( 889 |
890 |
891 |
892 | {user.name} 897 |
898 |

{user.name}

899 |

{user.email}

900 |
901 |
902 |
903 |
904 |

905 | User ID: {user.id} 906 |

907 |
908 |
909 | ); 910 | }; 911 | 912 | export default UserProfile; 913 | `, 914 | }, 915 | ]; 916 | 917 | function App() { 918 | return ( 919 | 925 | ); 926 | } 927 | ``` 928 | 929 | ### With Custom Error Handling 930 | 931 | ```tsx 932 | import { CodeExecutor } from "react-exe"; 933 | 934 | function App() { 935 | return ( 936 | { 946 | console.error("Component error:", error); 947 | // Send to error tracking service 948 | trackError(error); 949 | }, 950 | // Custom security patterns 951 | securityPatterns: [ 952 | /localStorage/i, 953 | /sessionStorage/i, 954 | /window\.location/i, 955 | ], 956 | }} 957 | /> 958 | ); 959 | } 960 | ``` 961 | 962 | ## Configuration Options 963 | 964 | The `config` prop accepts the following options: 965 | 966 | ```typescript 967 | interface CodeExecutorConfig { 968 | // External dependencies available to the rendered component 969 | dependencies?: Record; 970 | 971 | // Enable Tailwind CSS support 972 | enableTailwind?: boolean; 973 | 974 | // Custom className for the container 975 | containerClassName?: string; 976 | 977 | // Custom inline styles for the container 978 | containerStyle?: React.CSSProperties; 979 | 980 | // Custom className for error messages 981 | errorClassName?: string; 982 | 983 | // Custom inline styles for error messages 984 | errorStyle?: React.CSSProperties; 985 | 986 | // Custom security patterns to block potentially malicious code 987 | securityPatterns?: RegExp[]; 988 | 989 | // Error callback function 990 | onError?: (error: Error) => void; 991 | } 992 | ``` 993 | 994 | ## Code Input Types 995 | 996 | React-EXE accepts code in two formats: 997 | 998 | 1. **Single File**: Pass a string containing the React component code 999 | 1000 | ```typescript 1001 | // Single file as a string 1002 | const code = ` 1003 | export default function App() { 1004 | return
Hello World
; 1005 | } 1006 | `; 1007 | ``` 1008 | 1009 | 2. **Multiple Files**: Pass an array of CodeFile objects: 1010 | 1011 | ```typescript 1012 | // Multiple files 1013 | const code = [ 1014 | { 1015 | name: "App.tsx", 1016 | content: 1017 | "import React from 'react';\nimport Button from './Button';\n...", 1018 | isEntry: true, // Mark this as the entry point 1019 | }, 1020 | { 1021 | name: "Button.tsx", 1022 | content: 1023 | "export default function Button() { return ; }", 1024 | }, 1025 | ]; 1026 | ``` 1027 | 1028 | The `CodeFile` interface: 1029 | 1030 | ```typescript 1031 | interface CodeFile { 1032 | name: string; // File name with extension (used for imports) 1033 | content: string; // File content 1034 | isEntry?: boolean; // Whether this is the entry point (defaults to first file if not specified) 1035 | } 1036 | ``` 1037 | 1038 | ## Security 1039 | 1040 | React-EXE includes built-in security measures: 1041 | 1042 | - Default security patterns to block potentially harmful code 1043 | - Custom security pattern support 1044 | - Error boundary protection 1045 | 1046 | Default blocked patterns include: 1047 | 1048 | ```typescript 1049 | const defaultSecurityPatterns = [ 1050 | /document\.cookie/i, 1051 | /window\.document\.cookie/i, 1052 | /eval\(/i, 1053 | /Function\(/i, 1054 | /document\.write/i, 1055 | /document\.location/i, 1056 | ]; 1057 | ``` 1058 | 1059 | ## TypeScript Support 1060 | 1061 | React-EXE is written in TypeScript and includes type definitions. For the best development experience, use TypeScript in your project: 1062 | 1063 | ```tsx 1064 | import { CodeExecutor, CodeExecutorConfig, CodeFile } from "react-exe"; 1065 | 1066 | const config: CodeExecutorConfig = { 1067 | enableTailwind: true, 1068 | dependencies: { 1069 | "my-component": MyComponent, 1070 | }, 1071 | }; 1072 | 1073 | const files: CodeFile[] = [ 1074 | { 1075 | name: "App.tsx", 1076 | content: `export default function App() { return
Hello
; }`, 1077 | isEntry: true, 1078 | }, 1079 | ]; 1080 | 1081 | function App() { 1082 | return ; 1083 | } 1084 | ``` 1085 | 1086 | ## Used By [TuneChat](https://chat.tune.app/) to render Artifacts 1087 | 1088 | Screenshot 2025-02-26 at 16 58 34 1089 | 1090 | ## License 1091 | 1092 | MIT © [Vikrant](https://www.linkedin.com/in/vikrant-guleria/) 1093 | 1094 | --- 1095 | 1096 | Made with ❤️ by [Vikrant](https://www.linkedin.com/in/vikrant-guleria/) 1097 | --------------------------------------------------------------------------------