1.0.11 • Published 4 months ago

react-exe v1.0.11

Weekly downloads
-
License
MIT
Repository
github
Last release
4 months ago

React-EXE

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.

Try the live demo here.

Features

  • 🚀 Execute React components from string code
  • 📦 Support for external dependencies
  • 🎨 Tailwind CSS support
  • 🔒 Built-in security checks
  • 💅 Customizable styling
  • 📝 TypeScript support
  • ⚡ Live rendering
  • 🐛 Error boundary protection
  • 📄 Multi-file support

Installation

npm install react-exe
# or
yarn add react-exe
# or
pnpm add react-exe

Basic Usage

import { CodeExecutor } from "react-exe";

const code = `
export default function HelloWorld() {
  return (
    <div className="p-4 bg-blue-100 rounded">
      <h1 className="text-2xl font-bold">Hello World!</h1>
    </div>
  );
}
`;

function App() {
  return <CodeExecutor code={code} config={{ enableTailwind: true }} />;
}

Advanced Usage

With External Dependencies

import { CodeExecutor } from "react-exe";
import * as echarts from "echarts";
import * as framerMotion from "framer-motion";

const code = `
import { motion } from 'framer-motion';
import { LineChart } from 'echarts';

export default function Dashboard() {
  return (
    <motion.div
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      className="p-6 space-y-4"
    >
      <LineChart 
        option={{
          xAxis: { type: 'category', data: ['Mon', 'Tue', 'Wed'] },
          yAxis: { type: 'value' },
          series: [{ data: [150, 230, 224], type: 'line' }]
        }}
        style={{ height: '300px' }}
      />
    </motion.div>
  );
}
`;

function App() {
  return (
    <CodeExecutor
      code={code}
      config={{
        dependencies: {
          "framer-motion": framerMotion,
          echarts: echarts,
        },
        enableTailwind: true,
        containerClassName: "min-h-[400px]",
        containerStyle: {
          padding: "20px",
          background: "#f9fafb",
        },
      }}
    />
  );
}

With absolute imports and wildcard patterns

import { CodeExecutor } from "react-exe";
import * as echarts from "echarts";
import * as framerMotion from "framer-motion";
import * as uiComponents from "../ShadcnComps";

const code = `
import { motion } from 'framer-motion';
import { LineChart } from 'echarts';
import { Button } from "@/components/ui/button"

export default function Dashboard() {
  return (
    <motion.div
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      className="p-6 space-y-4"
    >
      <LineChart 
        option={{
          xAxis: { type: 'category', data: ['Mon', 'Tue', 'Wed'] },
          yAxis: { type: 'value' },
          series: [{ data: [150, 230, 224], type: 'line' }]
        }}
        style={{ height: '300px' }}
      />
    </motion.div>
  );
}
`;

function App() {
  return (
    <CodeExecutor
      code={code}
      config={{
        dependencies: {
          "framer-motion": framerMotion,
          echarts: echarts,
          "@/components/ui/*": uiComponents,
        },
        enableTailwind: true,
        containerClassName: "min-h-[400px]",
        containerStyle: {
          padding: "20px",
          background: "#f9fafb",
        },
      }}
    />
  );
}

With Multiple Files

React-EXE supports multiple files with cross-imports, allowing you to build more complex components and applications:

import { CodeExecutor } from "react-exe";
import * as framerMotion from "framer-motion";

// Define multiple files as an array of code files
const files = [
  {
    name: "App.tsx", // Main entry file
    content: `
import React from 'react';
import { motion } from 'framer-motion';
import Header from './Header';
import Counter from './Counter';

const App = () => {
  return (
    <motion.div 
      className="min-h-screen bg-gray-100 p-4"
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
    >
      <Header title="Multi-File App Example" />
      <Counter />
    </motion.div>
  );
};

export default App;
    `,
    isEntry: true, // Mark this as the entry point
  },
  {
    name: "Header.tsx",
    content: `
import React from 'react';

interface HeaderProps {
  title: string;
}

const Header = ({ title }: HeaderProps) => {
  return (
    <header className="bg-white p-4 mb-4 rounded-lg shadow-md">
      <h1 className="text-2xl font-bold text-gray-800">{title}</h1>
    </header>
  );
};

export default Header;
    `,
  },
  {
    name: "Counter.tsx",
    content: `
import React, { useState } from 'react';
import { motion } from 'framer-motion';
import CounterButton from './CounterButton';

const Counter = () => {
  const [count, setCount] = useState(0);
  
  const increment = () => setCount(prev => prev + 1);
  const decrement = () => setCount(prev => prev - 1);
  
  return (
    <div className="bg-white p-6 rounded-lg shadow-md">
      <h2 className="text-xl font-semibold mb-4">Counter Component</h2>
      
      <motion.div 
        className="text-center text-3xl font-bold my-4"
        key={count}
        initial={{ scale: 0.8, opacity: 0 }}
        animate={{ scale: 1, opacity: 1 }}
      >
        {count}
      </motion.div>
      
      <div className="flex justify-center gap-4">
        <CounterButton onClick={decrement} label="Decrease" variant="danger" />
        <CounterButton onClick={increment} label="Increase" variant="success" />
      </div>
    </div>
  );
};

export default Counter;
    `,
  },
  {
    name: "CounterButton.tsx",
    content: `
import React from 'react';
import { motion } from 'framer-motion';

interface CounterButtonProps {
  onClick: () => void;
  label: string;
  variant?: 'primary' | 'success' | 'danger';
}

const CounterButton = ({ 
  onClick, 
  label, 
  variant = 'primary' 
}: CounterButtonProps) => {
  
  const getButtonColor = () => {
    switch(variant) {
      case 'success': return 'bg-green-500 hover:bg-green-600';
      case 'danger': return 'bg-red-500 hover:bg-red-600';
      default: return 'bg-blue-500 hover:bg-blue-600';
    }
  };
  
  return (
    <motion.button
      className={\`\${getButtonColor()} text-white py-2 px-4 rounded\`}
      onClick={onClick}
      whileHover={{ scale: 1.05 }}
      whileTap={{ scale: 0.95 }}
    >
      {label}
    </motion.button>
  );
};

export default CounterButton;
    `,
  },
];

function App() {
  return (
    <CodeExecutor
      code={files}
      config={{
        dependencies: {
          "framer-motion": framerMotion,
        },
        enableTailwind: true,
        containerClassName: "rounded-lg overflow-hidden",
      }}
    />
  );
}

Creating a Project Structure with Multiple Files

For more complex applications, you can organize your files in a project-like structure:

import { CodeExecutor } from "react-exe";
import * as reactRouter from "react-router-dom";
import * as framerMotion from "framer-motion";

const files = [
  {
    name: "App.tsx",
    content: `
import React from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Layout from './components/Layout';
import Home from './pages/Home';
import About from './pages/About';
import NotFound from './pages/NotFound';

const App = () => {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Layout />}>
          <Route index element={<Home />} />
          <Route path="about" element={<About />} />
          <Route path="*" element={<NotFound />} />
        </Route>
      </Routes>
    </BrowserRouter>
  );
};

export default App;
    `,
    isEntry: true,
  },
  {
    name: "components/Layout.tsx",
    content: `
import React from 'react';
import { Outlet } from 'react-router-dom';
import Navbar from './Navbar';
import Footer from './Footer';

const Layout = () => {
  return (
    <div className="min-h-screen flex flex-col">
      <Navbar />
      <main className="flex-grow container mx-auto px-4 py-8">
        <Outlet />
      </main>
      <Footer />
    </div>
  );
};

export default Layout;
    `,
  },
  {
    name: "components/Navbar.tsx",
    content: `
import React from 'react';
import { Link, useLocation } from 'react-router-dom';

const Navbar = () => {
  const location = useLocation();
  
  const isActive = (path: string) => {
    return location.pathname === path ? 
      'text-white bg-indigo-700' : 
      'text-indigo-200 hover:text-white hover:bg-indigo-600';
  };
  
  return (
    <nav className="bg-indigo-800 text-white shadow-md">
      <div className="container mx-auto px-4">
        <div className="flex justify-between items-center h-16">
          <Link to="/" className="font-bold text-xl">Multi-File App</Link>
          
          <div className="flex space-x-4">
            <Link 
              to="/" 
              className={\`px-3 py-2 rounded-md \${isActive('/')}\`}
            >
              Home
            </Link>
            <Link 
              to="/about" 
              className={\`px-3 py-2 rounded-md \${isActive('/about')}\`}
            >
              About
            </Link>
          </div>
        </div>
      </div>
    </nav>
  );
};

export default Navbar;
    `,
  },
  {
    name: "components/Footer.tsx",
    content: `
import React from 'react';

const Footer = () => {
  return (
    <footer className="bg-gray-800 text-white py-6">
      <div className="container mx-auto px-4 text-center">
        <p>&copy; {new Date().getFullYear()} React-EXE Demo</p>
        <p className="text-gray-400 text-sm mt-1">Built with multiple files</p>
      </div>
    </footer>
  );
};

export default Footer;
    `,
  },
  {
    name: "pages/Home.tsx",
    content: `
import React from 'react';
import { motion } from 'framer-motion';

const Home = () => {
  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.5 }}
    >
      <h1 className="text-3xl font-bold mb-6">Welcome to the Home Page</h1>
      <p className="mb-4">This is a multi-file application example using React-EXE.</p>
      <p className="mb-4">
        It demonstrates how you can create complex applications with multiple
        components, pages, and even routing!
      </p>
      
      <div className="mt-8 p-6 bg-indigo-50 rounded-lg shadow-sm">
        <h2 className="text-xl font-semibold mb-4">Features Demonstrated:</h2>
        <ul className="list-disc pl-5 space-y-2">
          <li>Multiple file structure</li>
          <li>React Router integration</li>
          <li>Animation with Framer Motion</li>
          <li>Component composition</li>
          <li>Styling with Tailwind CSS</li>
        </ul>
      </div>
    </motion.div>
  );
};

export default Home;
    `,
  },
  {
    name: "pages/About.tsx",
    content: `
import React from 'react';
import { motion } from 'framer-motion';

const About = () => {
  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.5 }}
    >
      <h1 className="text-3xl font-bold mb-6">About Page</h1>
      <p className="mb-4">
        React-EXE is a powerful library for executing React components on the fly.
        It supports multi-file applications like this one!
      </p>
      
      <motion.div 
        className="mt-8 grid grid-cols-1 md:grid-cols-3 gap-4"
        variants={{
          hidden: { opacity: 0 },
          show: {
            opacity: 1,
            transition: {
              staggerChildren: 0.2
            }
          }
        }}
        initial="hidden"
        animate="show"
      >
        {[1, 2, 3].map((item) => (
          <motion.div
            key={item}
            className="bg-white p-6 rounded-lg shadow-md"
            variants={{
              hidden: { opacity: 0, y: 20 },
              show: { opacity: 1, y: 0 }
            }}
          >
            <h3 className="font-bold text-lg mb-2">Feature {item}</h3>
            <p className="text-gray-600">
              This is an example of a card that demonstrates Framer Motion animations
              in a multi-file React component.
            </p>
          </motion.div>
        ))}
      </motion.div>
    </motion.div>
  );
};

export default About;
    `,
  },
  {
    name: "pages/NotFound.tsx",
    content: `
import React from 'react';
import { Link } from 'react-router-dom';
import { motion } from 'framer-motion';

const NotFound = () => {
  return (
    <motion.div 
      className="text-center py-12"
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      transition={{ duration: 0.5 }}
    >
      <motion.div
        initial={{ scale: 0.8 }}
        animate={{ scale: 1 }}
        transition={{ 
          type: "spring", 
          stiffness: 200, 
          damping: 10 
        }}
      >
        <h1 className="text-9xl font-bold text-indigo-200">404</h1>
      </motion.div>
      
      <h2 className="text-3xl font-bold mb-4">Page Not Found</h2>
      <p className="text-gray-600 mb-8">
        The page you're looking for doesn't exist or has been moved.
      </p>
      
      <Link 
        to="/" 
        className="bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700 transition-colors"
      >
        Return Home
      </Link>
    </motion.div>
  );
};

export default NotFound;
    `,
  },
];

function App() {
  return (
    <CodeExecutor
      code={files}
      config={{
        dependencies: {
          "react-router-dom": reactRouter,
          "framer-motion": framerMotion,
        },
        enableTailwind: true,
      }}
    />
  );
}

Using Custom Hooks and Utilities in Multi-File Apps

You can also create and use custom hooks, utilities, and TypeScript types across multiple files:

import { CodeExecutor } from "react-exe";

const files = [
  {
    name: "App.tsx",
    content: `
import React from 'react';
import ThemeProvider from './theme/ThemeProvider';
import ThemeSwitcher from './components/ThemeSwitcher';
import UserProfile from './components/UserProfile';
import { fetchUserData } from './utils/api';

const App = () => {
  return (
    <ThemeProvider>
      <div className="min-h-screen p-6">
        <div className="max-w-lg mx-auto">
          <div className="flex justify-end mb-6">
            <ThemeSwitcher />
          </div>
          <UserProfile userId="1" fetchUserData={fetchUserData} />
        </div>
      </div>
    </ThemeProvider>
  );
};

export default App;
    `,
    isEntry: true,
  },
  {
    name: "types/index.ts",
    content: `
export interface User {
  id: string;
  name: string;
  email: string;
  avatar: string;
}

export type Theme = 'light' | 'dark' | 'system';

export interface ThemeContextType {
  theme: Theme;
  setTheme: (theme: Theme) => void;
}
    `,
  },
  {
    name: "theme/ThemeProvider.tsx",
    content: `
import React, { createContext, useContext, useState, useEffect } from 'react';
import { Theme, ThemeContextType } from '../types';

const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const [theme, setTheme] = useState<Theme>('system');
  
  useEffect(() => {
    const applyTheme = (newTheme: Theme) => {
      const root = window.document.documentElement;
      
      // Remove any existing theme classes
      root.classList.remove('light', 'dark');
      
      // Apply the appropriate theme
      if (newTheme === 'system') {
        const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
        root.classList.add(systemTheme);
      } else {
        root.classList.add(newTheme);
      }
    };
    
    applyTheme(theme);
    
    // Listen for system theme changes
    const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
    const handleChange = () => {
      if (theme === 'system') {
        applyTheme('system');
      }
    };
    
    mediaQuery.addEventListener('change', handleChange);
    return () => mediaQuery.removeEventListener('change', handleChange);
  }, [theme]);
  
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
};

export const useTheme = () => {
  const context = useContext(ThemeContext);
  if (context === undefined) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  return context;
};

export default ThemeProvider;
    `,
  },
  {
    name: "components/ThemeSwitcher.tsx",
    content: `
import React from 'react';
import { useTheme } from '../theme/ThemeProvider';
import { Theme } from '../types';

const ThemeSwitcher = () => {
  const { theme, setTheme } = useTheme();
  
  const themes: { value: Theme; label: string }[] = [
    { value: 'light', label: '☀️ Light' },
    { value: 'dark', label: '🌙 Dark' },
    { value: 'system', label: '🖥️ System' }
  ];
  
  return (
    <div className="bg-white dark:bg-gray-800 p-3 rounded-lg shadow-md inline-block">
      <div className="flex space-x-2">
        {themes.map(({ value, label }) => (
          <button
            key={value}
            onClick={() => setTheme(value)}
            className={\`px-3 py-1 rounded-md \${
              theme === value
                ? 'bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-200'
                : 'hover:bg-gray-100 dark:hover:bg-gray-700'
            }\`}
          >
            {label}
          </button>
        ))}
      </div>
    </div>
  );
};

export default ThemeSwitcher;
    `,
  },
  {
    name: "hooks/useUser.ts",
    content: `
import { useState, useEffect } from 'react';
import { User } from '../types';

export const useUser = (
  userId: string,
  fetchUserData: (id: string) => Promise<User>
) => {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  
  useEffect(() => {
    let isMounted = true;
    
    const loadUser = async () => {
      try {
        setLoading(true);
        const userData = await fetchUserData(userId);
        
        if (isMounted) {
          setUser(userData);
          setError(null);
        }
      } catch (err) {
        if (isMounted) {
          setError('Failed to load user');
          setUser(null);
        }
      } finally {
        if (isMounted) {
          setLoading(false);
        }
      }
    };
    
    loadUser();
    
    return () => {
      isMounted = false;
    };
  }, [userId, fetchUserData]);
  
  return { user, loading, error };
};
    `,
  },
  {
    name: "utils/api.ts",
    content: `
import { User } from '../types';

// Simulate API call with mock data
export const fetchUserData = async (userId: string): Promise<User> => {
  // Simulate network delay
  await new Promise(resolve => setTimeout(resolve, 1000));
  
  // Mock data
  const users: Record<string, User> = {
    '1': {
      id: '1',
      name: 'John Doe',
      email: 'john@example.com',
      avatar: 'https://randomuser.me/api/portraits/men/32.jpg'
    },
    '2': {
      id: '2',
      name: 'Jane Smith',
      email: 'jane@example.com',
      avatar: 'https://randomuser.me/api/portraits/women/44.jpg'
    }
  };
  
  const user = users[userId];
  
  if (!user) {
    throw new Error(\`User with ID \${userId} not found\`);
  }
  
  return user;
};
    `,
  },
  {
    name: "components/UserProfile.tsx",
    content: `
import React from 'react';
import { useUser } from '../hooks/useUser';
import { User } from '../types';

interface UserProfileProps {
  userId: string;
  fetchUserData: (id: string) => Promise<User>;
}

const UserProfile = ({ userId, fetchUserData }: UserProfileProps) => {
  const { user, loading, error } = useUser(userId, fetchUserData);
  
  if (loading) {
    return (
      <div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 animate-pulse">
        <div className="flex items-center space-x-4">
          <div className="rounded-full bg-gray-300 dark:bg-gray-600 h-16 w-16"></div>
          <div className="flex-1 space-y-3">
            <div className="h-4 bg-gray-300 dark:bg-gray-600 rounded w-3/4"></div>
            <div className="h-3 bg-gray-300 dark:bg-gray-600 rounded w-1/2"></div>
          </div>
        </div>
      </div>
    );
  }
  
  if (error) {
    return (
      <div className="bg-red-100 dark:bg-red-900 border-l-4 border-red-500 text-red-700 dark:text-red-200 p-4 rounded">
        <p>{error}</p>
      </div>
    );
  }
  
  if (!user) {
    return <div>No user found</div>;
  }
  
  return (
    <div className="bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden">
      <div className="p-6">
        <div className="flex items-center space-x-4">
          <img 
            src={user.avatar} 
            alt={user.name} 
            className="h-16 w-16 rounded-full border-2 border-indigo-500"
          />
          <div>
            <h2 className="text-xl font-bold text-gray-900 dark:text-white">{user.name}</h2>
            <p className="text-gray-600 dark:text-gray-300">{user.email}</p>
          </div>
        </div>
      </div>
      <div className="bg-gray-50 dark:bg-gray-900 px-6 py-4">
        <p className="text-sm text-gray-500 dark:text-gray-400">
          User ID: {user.id}
        </p>
      </div>
    </div>
  );
};

export default UserProfile;
    `,
  },
];

function App() {
  return (
    <CodeExecutor
      code={files}
      config={{
        enableTailwind: true,
      }}
    />
  );
}

With Custom Error Handling

import { CodeExecutor } from "react-exe";

function App() {
  return (
    <CodeExecutor
      code={code}
      config={{
        enableTailwind: true,
        errorClassName: "my-error-class",
        errorStyle: {
          background: "#fee2e2",
          border: "2px solid #ef4444",
        },
        onError: (error) => {
          console.error("Component error:", error);
          // Send to error tracking service
          trackError(error);
        },
        // Custom security patterns
        securityPatterns: [
          /localStorage/i,
          /sessionStorage/i,
          /window\.location/i,
        ],
      }}
    />
  );
}

Configuration Options

The config prop accepts the following options:

interface CodeExecutorConfig {
  // External dependencies available to the rendered component
  dependencies?: Record<string, any>;

  // Enable Tailwind CSS support
  enableTailwind?: boolean;

  // Custom className for the container
  containerClassName?: string;

  // Custom inline styles for the container
  containerStyle?: React.CSSProperties;

  // Custom className for error messages
  errorClassName?: string;

  // Custom inline styles for error messages
  errorStyle?: React.CSSProperties;

  // Custom security patterns to block potentially malicious code
  securityPatterns?: RegExp[];

  // Error callback function
  onError?: (error: Error) => void;
}

Code Input Types

React-EXE accepts code in two formats:

  1. Single File: Pass a string containing the React component code

    // Single file as a string
    const code = `
    export default function App() {
      return <div>Hello World</div>;
    }
    `;
  2. Multiple Files: Pass an array of CodeFile objects:

    // Multiple files
    const code = [
      {
        name: "App.tsx",
        content:
          "import React from 'react';\nimport Button from './Button';\n...",
        isEntry: true, // Mark this as the entry point
      },
      {
        name: "Button.tsx",
        content:
          "export default function Button() { return <button>Click me</button>; }",
      },
    ];

    The CodeFile interface:

    interface CodeFile {
      name: string; // File name with extension (used for imports)
      content: string; // File content
      isEntry?: boolean; // Whether this is the entry point (defaults to first file if not specified)
    }

Security

React-EXE includes built-in security measures:

  • Default security patterns to block potentially harmful code
  • Custom security pattern support
  • Error boundary protection

Default blocked patterns include:

const defaultSecurityPatterns = [
  /document\.cookie/i,
  /window\.document\.cookie/i,
  /eval\(/i,
  /Function\(/i,
  /document\.write/i,
  /document\.location/i,
];

TypeScript Support

React-EXE is written in TypeScript and includes type definitions. For the best development experience, use TypeScript in your project:

import { CodeExecutor, CodeExecutorConfig, CodeFile } from "react-exe";

const config: CodeExecutorConfig = {
  enableTailwind: true,
  dependencies: {
    "my-component": MyComponent,
  },
};

const files: CodeFile[] = [
  {
    name: "App.tsx",
    content: `export default function App() { return <div>Hello</div>; }`,
    isEntry: true,
  },
];

function App() {
  return <CodeExecutor code={files} config={config} />;
}

License

MIT © Vikrant


Made with ❤️ by Vikrant

1.0.11

4 months ago

1.0.10

4 months ago

1.0.9

5 months ago

1.0.8

5 months ago

1.0.7

5 months ago

1.0.6-1

5 months ago

1.0.6

5 months ago

1.0.5

5 months ago

1.0.4-1

5 months ago

1.0.4

5 months ago

1.0.3

5 months ago

1.0.2

5 months ago

1.0.0-6

5 months ago

1.0.0-5

5 months ago

1.0.0-4

5 months ago

1.0.0-3

5 months ago

1.0.0-2

5 months ago

1.0.0-1

5 months ago

1.0.0

5 months ago