├── .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 |
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 |
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 |
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 |
873 | );
874 | }
875 |
876 | if (error) {
877 | return (
878 |
881 | );
882 | }
883 |
884 | if (!user) {
885 | return No user found
;
886 | }
887 |
888 | return (
889 |
890 |
891 |
892 |

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 |
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 |
--------------------------------------------------------------------------------