@aquiles-ai/renderize 1.85.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,2 +1,148 @@
1
+ <div align="center">
2
+
1
3
  # Renderize
4
+
5
+ <img src="https://res.cloudinary.com/dmtomxyvm/image/upload/v1772919461/renderize_z4qlyx.png" alt="Renderize" width="800"/>
6
+
2
7
  Drop-in sandbox component that executes AI-generated React code with zero configuration.
8
+
9
+ ```tsx
10
+ <Renderize code={llmGeneratedCode} />
11
+ ```
12
+
13
+ </div>
14
+
15
+ ## How it works
16
+
17
+ Renderize injects LLM-generated code into a `srcdoc` iframe that comes pre-loaded with React 18, Tailwind CSS, Babel standalone, and a curated set of UI libraries. The iframe runs without `allow-same-origin`, so it can't access the parent page's cookies, storage, or DOM.
18
+
19
+ A built-in fetch proxy bridges the null-origin restriction: the iframe posts fetch requests to the parent window, which executes them and sends the response back. This means AI-generated code can call external APIs without any CORS configuration.
20
+
21
+ ```
22
+ Parent window
23
+
24
+ ├── <Renderize /> mounts the iframe, owns the message bus
25
+ │ ├── fetch proxy relays fetch calls from iframe → real network
26
+ │ └── error forwarding surfaces runtime errors via onError()
27
+
28
+ └── srcdoc iframe (sandboxed)
29
+ ├── React 18 + ReactDOM
30
+ ├── Tailwind CSS (CDN)
31
+ ├── Babel standalone transpiles JSX + modern JS at runtime
32
+ ├── importmap resolves lucide-react, @radix-ui/*, etc.
33
+ └── App() your LLM-generated component
34
+ ```
35
+
36
+ ## Installation
37
+
38
+ ```bash
39
+ npm i @aquiles-ai/renderize
40
+ # or
41
+ pnpm add @aquiles-ai/renderize
42
+ ```
43
+
44
+ ## Usage
45
+
46
+ ```tsx
47
+ import { Renderize } from "@aquiles-ai/renderize";
48
+
49
+ export default function Playground() {
50
+ const code = `
51
+ function App() {
52
+ const [count, setCount] = useState(0);
53
+ return (
54
+ <div className="flex flex-col items-center gap-4 p-8">
55
+ <h1 className="text-2xl font-bold">{count}</h1>
56
+ <button
57
+ onClick={() => setCount(c => c + 1)}
58
+ className="px-4 py-2 bg-blue-600 text-white rounded"
59
+ >
60
+ Increment
61
+ </button>
62
+ </div>
63
+ );
64
+ }
65
+ `;
66
+
67
+ return <Renderize code={code} height={400} />;
68
+ }
69
+ ```
70
+
71
+ <div align="center">
72
+
73
+ ## Example of integration
74
+
75
+ <img src="https://res.cloudinary.com/dmtomxyvm/image/upload/v1772990544/Captura_de_pantalla_2157_ziyjnu.png" alt="Example1" width="600"/>
76
+
77
+ <br><br>
78
+
79
+ <img src="https://res.cloudinary.com/dmtomxyvm/image/upload/v1772990545/Captura_de_pantalla_2158_b3e1up.png" alt="Example2" width="600"/>
80
+
81
+ <br><br>
82
+
83
+ <img src="https://res.cloudinary.com/dmtomxyvm/image/upload/v1772990556/Captura_de_pantalla_2159_azlagy.png" alt="Example3" width="600"/>
84
+
85
+ </div>
86
+
87
+ ## Props
88
+
89
+ | Prop | Type | Default | Description |
90
+ |------|------|---------|-------------|
91
+ | `code` | `string` | | React code generated by the LLM. Must define a function component named `App`. |
92
+ | `height` | `string \| number` | `"100%"` | Height of the sandbox iframe. |
93
+ | `width` | `string \| number` | `"100%"` | Width of the sandbox iframe. |
94
+ | `className` | `string` | | Class name for the wrapper `<div>`. |
95
+ | `style` | `React.CSSProperties` | | Inline styles for the wrapper `<div>`. |
96
+ | `onError` | `(error: string) => void` | | Called when the sandbox encounters a runtime error. |
97
+
98
+ ## Available libraries
99
+
100
+ The sandbox importmap includes the following packages. Imports from any other module are stripped by `sanitizeCode`.
101
+
102
+ | Package | Notes |
103
+ |---------|-------|
104
+ | `react` | v18, hooks already in global scope |
105
+ | `react-dom` | v18 |
106
+ | `lucide-react` | Icon library |
107
+ | `clsx` | Class name utility |
108
+ | `tailwind-merge` | Tailwind class merging |
109
+ | `class-variance-authority` | CVA, variant-based styling |
110
+ | `@radix-ui/react-*` | Full Radix UI primitives suite |
111
+
112
+ React hooks (`useState`, `useEffect`, `useRef`, `useCallback`, `useMemo`, `useReducer`, `useContext`, `createContext`, `forwardRef`, `Fragment`) are already imported and available in global scope. The LLM does not need to import them.
113
+
114
+ ## Requirements for LLM-generated code
115
+
116
+ The component name must be `App`. The template calls `React.createElement(App)` directly, so:
117
+
118
+ ```tsx
119
+ // Correct
120
+ function App() { ... }
121
+
122
+ // Also correct (sanitizeCode will fix these automatically)
123
+ export default function App() { ... }
124
+ export default function Dashboard() { ... }
125
+ function MyComponent() { ... } // if it's the only PascalCase component
126
+ ```
127
+
128
+ ## Sandbox security
129
+
130
+ The iframe uses this `sandbox` attribute:
131
+
132
+ ```
133
+ allow-scripts allow-forms allow-modals allow-popups allow-downloads
134
+ ```
135
+
136
+ `allow-same-origin` is intentionally omitted. This means:
137
+
138
+ - No access to `localStorage` or `sessionStorage`
139
+ - No access to `document.cookie`
140
+ - No access to `indexedDB`
141
+ - No access to the parent page's DOM
142
+
143
+ The fetch proxy is the only bridge between the iframe and the outside world.
144
+
145
+
146
+ ## License
147
+
148
+ Apache 2.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aquiles-ai/renderize",
3
- "version": "1.85.0",
3
+ "version": "2.1.0",
4
4
  "description": "Drop-in sandbox component that executes AI-generated React code with zero configuration.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
package/src/Renderize.tsx CHANGED
@@ -1,6 +1,7 @@
1
1
  /// <reference lib="dom" />
2
2
  import React, { useEffect, useRef, useState } from "react";
3
3
  import { buildTemplate } from "./template.js";
4
+ import { sanitizeCode } from "./sanitize.js";
4
5
 
5
6
  export interface RenderizeProps {
6
7
  /** React code generated by the LLM. Must define a function component named App. */
@@ -30,7 +31,7 @@ export function Renderize({
30
31
 
31
32
  useEffect(() => {
32
33
  if (!code?.trim()) return;
33
- setSrcDoc(buildTemplate(code));
34
+ setSrcDoc(buildTemplate(sanitizeCode(code)));
34
35
  }, [code]);
35
36
 
36
37
  // Central message handler — handles both fetch proxying and error forwarding
@@ -0,0 +1,297 @@
1
+ // ── Constants ────────────────────────────────────────────────────────────────
2
+
3
+ /**
4
+ * Modules already injected by the sandbox template (react, react-dom, etc.).
5
+ * Imports from these are always stripped.
6
+ */
7
+ const TEMPLATE_PROVIDED_MODULES = new Set([
8
+ "react",
9
+ "react-dom",
10
+ "react-dom/client",
11
+ "react/jsx-runtime",
12
+ ]);
13
+
14
+ /**
15
+ * Named bindings already in scope thanks to the template's explicit imports.
16
+ * If an import only pulls names from this set, the whole line is stripped.
17
+ */
18
+ const TEMPLATE_PROVIDED_NAMES = new Set([
19
+ "React",
20
+ "useState",
21
+ "useEffect",
22
+ "useRef",
23
+ "useCallback",
24
+ "useMemo",
25
+ "useReducer",
26
+ "useContext",
27
+ "createContext",
28
+ "forwardRef",
29
+ "Fragment",
30
+ "createRoot",
31
+ ]);
32
+
33
+ /**
34
+ * Every module present in the template's importmap.
35
+ * Imports from modules NOT in this set are stripped (they'd cause a network
36
+ * error or a module-resolution failure inside the srcdoc iframe).
37
+ */
38
+ const IMPORTMAP_MODULES = new Set([
39
+ "react",
40
+ "react/jsx-runtime",
41
+ "react-dom",
42
+ "react-dom/client",
43
+ "lucide-react",
44
+ "clsx",
45
+ "class-variance-authority",
46
+ "tailwind-merge",
47
+ "@radix-ui/react-accordion",
48
+ "@radix-ui/react-alert-dialog",
49
+ "@radix-ui/react-avatar",
50
+ "@radix-ui/react-checkbox",
51
+ "@radix-ui/react-collapsible",
52
+ "@radix-ui/react-context-menu",
53
+ "@radix-ui/react-dialog",
54
+ "@radix-ui/react-dropdown-menu",
55
+ "@radix-ui/react-hover-card",
56
+ "@radix-ui/react-label",
57
+ "@radix-ui/react-menubar",
58
+ "@radix-ui/react-navigation-menu",
59
+ "@radix-ui/react-popover",
60
+ "@radix-ui/react-progress",
61
+ "@radix-ui/react-radio-group",
62
+ "@radix-ui/react-scroll-area",
63
+ "@radix-ui/react-select",
64
+ "@radix-ui/react-separator",
65
+ "@radix-ui/react-slider",
66
+ "@radix-ui/react-slot",
67
+ "@radix-ui/react-switch",
68
+ "@radix-ui/react-tabs",
69
+ "@radix-ui/react-toast",
70
+ "@radix-ui/react-toggle",
71
+ "@radix-ui/react-toggle-group",
72
+ "@radix-ui/react-toolbar",
73
+ "@radix-ui/react-tooltip",
74
+ ]);
75
+
76
+ // ── Helpers ───────────────────────────────────────────────────────────────────
77
+
78
+ /**
79
+ * Given a string and the index of an opening brace, returns the index
80
+ * immediately AFTER the matching closing brace (or -1 if not found).
81
+ * Handles nesting and ignores braces inside string literals.
82
+ */
83
+ function findMatchingBrace(code: string, openIndex: number): number {
84
+ let depth = 0;
85
+ let inSingle = false;
86
+ let inDouble = false;
87
+ let inTemplate = 0;
88
+
89
+ for (let i = openIndex; i < code.length; i++) {
90
+ const ch = code[i];
91
+ const prev = i > 0 ? code[i - 1] : "";
92
+
93
+ if (prev === "\\") continue; // escaped — skip
94
+
95
+ if (!inDouble && !inTemplate && ch === "'") { inSingle = !inSingle; continue; }
96
+ if (!inSingle && !inTemplate && ch === '"') { inDouble = !inDouble; continue; }
97
+ if (!inSingle && !inDouble && ch === "`") {
98
+ inTemplate = inTemplate ? inTemplate - 1 : inTemplate + 1;
99
+ continue;
100
+ }
101
+ if (inSingle || inDouble || inTemplate) continue;
102
+
103
+ if (ch === "{") depth++;
104
+ else if (ch === "}") {
105
+ depth--;
106
+ if (depth === 0) return i + 1;
107
+ }
108
+ }
109
+ return -1;
110
+ }
111
+
112
+ /**
113
+ * Strips top-level TypeScript block declarations (interface / enum)
114
+ * that Babel standalone cannot parse without @babel/preset-typescript.
115
+ * Also strips `type X = ...` aliases (block or inline).
116
+ */
117
+ function stripTypeScriptDeclarations(code: string): string {
118
+ // ── interface Foo { ... } and enum Foo { ... } ─────────────────────────
119
+ const blockKeywords = /(?:export\s+)?(?:interface|enum)\s+\w[\w<,\s>]*\s*\{/g;
120
+ let match: RegExpExecArray | null;
121
+
122
+ while ((match = blockKeywords.exec(code)) !== null) {
123
+ const openBrace = code.indexOf("{", match.index + match[0].length - 1);
124
+ if (openBrace === -1) continue;
125
+ const end = findMatchingBrace(code, openBrace);
126
+ if (end === -1) continue;
127
+ const tail = code[end] === ";" ? end + 1 : end;
128
+ code = code.slice(0, match.index) + code.slice(tail);
129
+ blockKeywords.lastIndex = match.index;
130
+ }
131
+
132
+ // ── type Foo = { ... } or type Foo = string | number; ──────────────────
133
+ // Strip the header and then either the brace block or the rest of the line.
134
+ code = code.replace(
135
+ /^(?:export\s+)?type\s+\w[\w<,\s>]*\s*=\s*/gm,
136
+ (_header, offset, fullCode) => {
137
+ const rest = fullCode.slice(offset + _header.length);
138
+ const trimmed = rest.trimStart();
139
+ if (trimmed.startsWith("{")) {
140
+ const relOpen = rest.indexOf("{");
141
+ const end = findMatchingBrace(rest, relOpen);
142
+ if (end !== -1) {
143
+ code = fullCode.slice(0, offset) + fullCode.slice(offset + _header.length + end);
144
+ }
145
+ }
146
+ // For inline types, removing the header is enough; the value
147
+ // (e.g. "string | number;\n") becomes a no-op expression statement
148
+ // which Babel tolerates, so we don't need to strip it.
149
+ return "";
150
+ }
151
+ );
152
+
153
+ return code;
154
+ }
155
+
156
+ /**
157
+ * Strips TypeScript `as` type assertions from expressions.
158
+ * Carefully avoids import/export aliases like `import { x as y }`.
159
+ *
160
+ * (value as string) → (value)
161
+ * setState(count as number) → setState(count)
162
+ * const x = foo as Bar; → const x = foo;
163
+ *
164
+ * NOT touched:
165
+ * import { foo as bar } from "..."
166
+ * export { baz as default }
167
+ */
168
+ function stripAsAssertions(code: string): string {
169
+ return code.replace(
170
+ /(?<![{,]\s*\w+)\s+as\s+[A-Z]\w*(?:<[^>]*>)?(?:\[\])?(?=\s*[),;}\n])/g,
171
+ ""
172
+ );
173
+ }
174
+
175
+ /**
176
+ * Detects the name of the main React component the LLM defined, if it is not
177
+ * already "App". Returns null when renaming is not necessary.
178
+ *
179
+ * Priority order:
180
+ * 1. export default function X → X
181
+ * 2. export default const/let X = ... → X
182
+ * 3. Only PascalCase function/const visible at the top level → X
183
+ */
184
+ function detectMainComponentName(code: string): string | null {
185
+ const fnExport = code.match(/export\s+default\s+function\s+([A-Z]\w*)/);
186
+ // @ts-ignore
187
+ if (fnExport && fnExport[1] !== "App") return fnExport[1];
188
+
189
+ const constExport = code.match(/export\s+default\s+(?:const|let)\s+([A-Z]\w*)/);
190
+ // @ts-ignore
191
+ if (constExport && constExport[1] !== "App") return constExport[1];
192
+
193
+ // Fallback: single PascalCase function/const that is not App
194
+ const allDefs = [
195
+ ...code.matchAll(
196
+ /^(?:function|const|let)\s+([A-Z]\w*)\s*(?:=\s*(?:\(|React\.memo\()|[\(<(])/gm
197
+ ),
198
+ ].map((m) => m[1]).filter((n) => n !== "App");
199
+
200
+ // @ts-ignore
201
+ if (allDefs.length === 1) return allDefs[0];
202
+
203
+ return null;
204
+ }
205
+
206
+ // ── Main export ───────────────────────────────────────────────────────────────
207
+
208
+ /**
209
+ * Sanitizes LLM-generated React code before passing it to buildTemplate().
210
+ *
211
+ * Steps (in order):
212
+ * 1. Strip markdown code fences
213
+ * 2. Fix literal escape sequences (\\n, \\t, \\r)
214
+ * 3. Strip Next.js / RSC directives ('use client', 'use server')
215
+ * 4. Strip TypeScript-only syntax (interface, type, enum, `as` assertions)
216
+ * 5. Ensure the main component is named App and has no export keyword
217
+ * 6. Strip imports from modules outside the importmap
218
+ * 7. Strip CommonJS require() calls
219
+ * 8. Collapse excessive blank lines
220
+ */
221
+ export function sanitizeCode(raw: string): string {
222
+ let code = raw;
223
+
224
+ // ── 1. Strip markdown code fences ───────────────────────────────────────
225
+ code = code.replace(/^```[a-zA-Z]*\r?\n?/, "").replace(/\r?\n?```\s*$/, "");
226
+
227
+ // ── 2. Fix literal escape sequences ─────────────────────────────────────
228
+ code = code
229
+ .replace(/\\n/g, "\n")
230
+ .replace(/\\t/g, "\t")
231
+ .replace(/\\r/g, "\r");
232
+
233
+ // ── 3. Strip Next.js / RSC directives ───────────────────────────────────
234
+ code = code.replace(/^\s*['"]use (client|server)['"]\s*;?\s*\n?/gm, "");
235
+
236
+ // ── 4. Strip TypeScript-only syntax ─────────────────────────────────────
237
+ code = stripTypeScriptDeclarations(code);
238
+ code = stripAsAssertions(code);
239
+
240
+ // ── 5. Fix component name and remove export keywords ────────────────────
241
+ // The template calls React.createElement(App), so App must be a plain
242
+ // function in global scope — no export keyword.
243
+
244
+ const originalName = detectMainComponentName(code);
245
+ if (originalName) {
246
+ // Rename all occurrences of the original name to "App"
247
+ code = code.replace(new RegExp(`\\b${originalName}\\b`, "g"), "App");
248
+ }
249
+
250
+ // Strip "export default" prefix from the App declaration
251
+ code = code.replace(/\bexport\s+default\s+(function\s+App\b)/, "$1");
252
+ code = code.replace(/\bexport\s+default\s+((?:const|let)\s+App\b)/, "$1");
253
+
254
+ // Strip "export" (non-default) prefix from the App declaration
255
+ code = code.replace(/\bexport\s+(function\s+App\b)/, "$1");
256
+ code = code.replace(/\bexport\s+((?:const|let)\s+App\b)/, "$1");
257
+
258
+ // ── 6. Strip imports from modules outside the importmap ─────────────────
259
+ const importLineRegex =
260
+ /^import\s+(?:type\s+)?(?:[^"'\n]+\s+from\s+)?["']([^"']+)["'];?\s*$/gm;
261
+
262
+ code = code.replace(importLineRegex, (line, modulePath) => {
263
+ // Always strip template-provided modules (already in global scope)
264
+ if (TEMPLATE_PROVIDED_MODULES.has(modulePath)) return "";
265
+
266
+ // Strip anything outside the importmap (would cause a fetch/resolve error)
267
+ if (!IMPORTMAP_MODULES.has(modulePath)) return "";
268
+
269
+ // Strip if all named imports are already in scope from the template
270
+ const namedMatch = line.match(/\{([^}]+)\}/);
271
+ if (namedMatch) {
272
+ // @ts-ignore
273
+ const names = namedMatch[1]
274
+ .split(",")
275
+ // @ts-ignore
276
+ .map((n) => n.trim().split(/\s+as\s+/)[0].trim())
277
+ .filter(Boolean);
278
+ if (names.length > 0 && names.every((n) => TEMPLATE_PROVIDED_NAMES.has(n))) {
279
+ return "";
280
+ }
281
+ }
282
+
283
+ return line;
284
+ });
285
+
286
+ // ── 7. Strip CommonJS require() calls ───────────────────────────────────
287
+ // The sandbox runs as ESM; require() is undefined.
288
+ code = code.replace(
289
+ /^(?:const|let|var)\s+[\w\s,{}]+\s*=\s*require\s*\(["'][^"']+["']\)\s*;?\s*$/gm,
290
+ ""
291
+ );
292
+
293
+ // ── 8. Collapse excessive blank lines ────────────────────────────────────
294
+ code = code.replace(/\n{3,}/g, "\n\n");
295
+
296
+ return code.trim();
297
+ }
package/src/template.ts CHANGED
@@ -53,6 +53,28 @@ export function buildTemplate(code: string): string {
53
53
  <style>
54
54
  * { box-sizing: border-box; }
55
55
  body { margin: 0; padding: 0; }
56
+
57
+ /* Scrollbar personalizada: fina y semi-transparente */
58
+ ::-webkit-scrollbar {
59
+ width: 6px;
60
+ height: 6px;
61
+ }
62
+ ::-webkit-scrollbar-track {
63
+ background: transparent;
64
+ }
65
+ ::-webkit-scrollbar-thumb {
66
+ background: rgba(255, 255, 255, 0.35);
67
+ border-radius: 999px;
68
+ transition: background 0.2s ease;
69
+ }
70
+ ::-webkit-scrollbar-thumb:hover {
71
+ background: rgba(255, 255, 255, 0.55);
72
+ }
73
+ /* Firefox */
74
+ * {
75
+ scrollbar-width: thin;
76
+ scrollbar-color: rgba(255, 255, 255, 0.35) transparent;
77
+ }
56
78
  <\/style>
57
79
  <\/head>
58
80
  <body>
package/dist/index.cjs DELETED
@@ -1,256 +0,0 @@
1
- "use strict";
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __hasOwnProp = Object.prototype.hasOwnProperty;
6
- var __export = (target, all) => {
7
- for (var name in all)
8
- __defProp(target, name, { get: all[name], enumerable: true });
9
- };
10
- var __copyProps = (to, from, except, desc) => {
11
- if (from && typeof from === "object" || typeof from === "function") {
12
- for (let key of __getOwnPropNames(from))
13
- if (!__hasOwnProp.call(to, key) && key !== except)
14
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
- }
16
- return to;
17
- };
18
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
-
20
- // src/index.ts
21
- var index_exports = {};
22
- __export(index_exports, {
23
- Renderize: () => Renderize
24
- });
25
- module.exports = __toCommonJS(index_exports);
26
-
27
- // src/Renderize.tsx
28
- var import_react = require("react");
29
-
30
- // src/template.ts
31
- function buildTemplate(code) {
32
- return `<!DOCTYPE html>
33
- <html lang="en">
34
- <head>
35
- <meta charset="UTF-8" />
36
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
37
-
38
- <script src="https://cdn.tailwindcss.com"></script>
39
- <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
40
-
41
- <script type="importmap">
42
- {
43
- "imports": {
44
- "react": "https://esm.sh/react@18",
45
- "react/jsx-runtime": "https://esm.sh/react@18/jsx-runtime",
46
- "react-dom": "https://esm.sh/react-dom@18",
47
- "react-dom/client": "https://esm.sh/react-dom@18/client",
48
- "lucide-react": "https://esm.sh/lucide-react?external=react",
49
- "clsx": "https://esm.sh/clsx",
50
- "class-variance-authority": "https://esm.sh/class-variance-authority",
51
- "tailwind-merge": "https://esm.sh/tailwind-merge",
52
- "@radix-ui/react-accordion": "https://esm.sh/@radix-ui/react-accordion?external=react,react-dom",
53
- "@radix-ui/react-alert-dialog": "https://esm.sh/@radix-ui/react-alert-dialog?external=react,react-dom",
54
- "@radix-ui/react-avatar": "https://esm.sh/@radix-ui/react-avatar?external=react,react-dom",
55
- "@radix-ui/react-checkbox": "https://esm.sh/@radix-ui/react-checkbox?external=react,react-dom",
56
- "@radix-ui/react-collapsible": "https://esm.sh/@radix-ui/react-collapsible?external=react,react-dom",
57
- "@radix-ui/react-context-menu": "https://esm.sh/@radix-ui/react-context-menu?external=react,react-dom",
58
- "@radix-ui/react-dialog": "https://esm.sh/@radix-ui/react-dialog?external=react,react-dom",
59
- "@radix-ui/react-dropdown-menu": "https://esm.sh/@radix-ui/react-dropdown-menu?external=react,react-dom",
60
- "@radix-ui/react-hover-card": "https://esm.sh/@radix-ui/react-hover-card?external=react,react-dom",
61
- "@radix-ui/react-label": "https://esm.sh/@radix-ui/react-label?external=react,react-dom",
62
- "@radix-ui/react-menubar": "https://esm.sh/@radix-ui/react-menubar?external=react,react-dom",
63
- "@radix-ui/react-navigation-menu": "https://esm.sh/@radix-ui/react-navigation-menu?external=react,react-dom",
64
- "@radix-ui/react-popover": "https://esm.sh/@radix-ui/react-popover?external=react,react-dom",
65
- "@radix-ui/react-progress": "https://esm.sh/@radix-ui/react-progress?external=react,react-dom",
66
- "@radix-ui/react-radio-group": "https://esm.sh/@radix-ui/react-radio-group?external=react,react-dom",
67
- "@radix-ui/react-scroll-area": "https://esm.sh/@radix-ui/react-scroll-area?external=react,react-dom",
68
- "@radix-ui/react-select": "https://esm.sh/@radix-ui/react-select?external=react,react-dom",
69
- "@radix-ui/react-separator": "https://esm.sh/@radix-ui/react-separator?external=react,react-dom",
70
- "@radix-ui/react-slider": "https://esm.sh/@radix-ui/react-slider?external=react,react-dom",
71
- "@radix-ui/react-slot": "https://esm.sh/@radix-ui/react-slot?external=react,react-dom",
72
- "@radix-ui/react-switch": "https://esm.sh/@radix-ui/react-switch?external=react,react-dom",
73
- "@radix-ui/react-tabs": "https://esm.sh/@radix-ui/react-tabs?external=react,react-dom",
74
- "@radix-ui/react-toast": "https://esm.sh/@radix-ui/react-toast?external=react,react-dom",
75
- "@radix-ui/react-toggle": "https://esm.sh/@radix-ui/react-toggle?external=react,react-dom",
76
- "@radix-ui/react-toggle-group": "https://esm.sh/@radix-ui/react-toggle-group?external=react,react-dom",
77
- "@radix-ui/react-toolbar": "https://esm.sh/@radix-ui/react-toolbar?external=react,react-dom",
78
- "@radix-ui/react-tooltip": "https://esm.sh/@radix-ui/react-tooltip?external=react,react-dom"
79
- }
80
- }
81
- </script>
82
-
83
- <style>
84
- * { box-sizing: border-box; }
85
- body { margin: 0; padding: 0; }
86
- </style>
87
- </head>
88
- <body>
89
- <div id="root"></div>
90
-
91
- <script>
92
- // \u2500\u2500 FETCH PROXY \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
93
- // Override window.fetch to proxy requests through the parent window.
94
- // This solves the CORS/null-origin issue with srcdoc iframes:
95
- // the parent has a real origin and can make fetch calls freely.
96
- window.fetch = function(url, options = {}) {
97
- return new Promise((resolve, reject) => {
98
- const id = Math.random().toString(36).slice(2) + Date.now().toString(36);
99
-
100
- // Serialize body \u2014 postMessage can't transfer Request objects
101
- const serializedOptions = {
102
- method: options.method || "GET",
103
- headers: options.headers || {},
104
- body: options.body || null,
105
- };
106
-
107
- // Listen for the response from the parent
108
- function handleMessage(event) {
109
- if (
110
- event.data?.source !== "renderize" ||
111
- event.data?.type !== "fetch-response" ||
112
- event.data?.id !== id
113
- ) return;
114
-
115
- window.removeEventListener("message", handleMessage);
116
-
117
- if (event.data.error) {
118
- reject(new Error(event.data.error));
119
- return;
120
- }
121
-
122
- // Reconstruct a real Response object from the serialized data
123
- const { status, statusText, headers, body } = event.data;
124
- const responseBody = typeof body === "string" ? body : JSON.stringify(body);
125
-
126
- const response = new Response(responseBody, {
127
- status,
128
- statusText,
129
- headers: new Headers(headers),
130
- });
131
-
132
- resolve(response);
133
- }
134
-
135
- window.addEventListener("message", handleMessage);
136
-
137
- // Ask the parent to perform the fetch on our behalf
138
- window.parent.postMessage({
139
- source: "renderize",
140
- type: "fetch-request",
141
- id,
142
- url,
143
- options: serializedOptions,
144
- }, "*");
145
- });
146
- };
147
- // \u2500\u2500 END FETCH PROXY \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
148
- </script>
149
-
150
- <script type="text/babel" data-type="module">
151
- import React, {
152
- useState, useEffect, useRef, useCallback,
153
- useMemo, useReducer, useContext, createContext,
154
- forwardRef, Fragment
155
- } from "react";
156
- import { createRoot } from "react-dom/client";
157
-
158
- // \u2500\u2500 USER CODE START \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
159
- ${code}
160
- // \u2500\u2500 USER CODE END \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
161
-
162
- const container = document.getElementById("root");
163
- createRoot(container).render(React.createElement(App));
164
- </script>
165
- </body>
166
- </html>`;
167
- }
168
-
169
- // src/Renderize.tsx
170
- var import_jsx_runtime = require("react/jsx-runtime");
171
- function Renderize({
172
- code,
173
- height = "100%",
174
- width = "100%",
175
- className,
176
- style,
177
- onError
178
- }) {
179
- const iframeRef = (0, import_react.useRef)(null);
180
- const [srcDoc, setSrcDoc] = (0, import_react.useState)(null);
181
- (0, import_react.useEffect)(() => {
182
- if (!code?.trim()) return;
183
- setSrcDoc(buildTemplate(code));
184
- }, [code]);
185
- (0, import_react.useEffect)(() => {
186
- const handler = async (event) => {
187
- if (event.data?.source !== "renderize") return;
188
- if (event.data.type === "fetch-request") {
189
- const { id, url, options } = event.data;
190
- try {
191
- const res = await fetch(url, {
192
- method: options.method,
193
- headers: options.headers,
194
- body: options.body ?? void 0
195
- });
196
- const body = await res.text();
197
- iframeRef.current?.contentWindow?.postMessage(
198
- {
199
- source: "renderize",
200
- type: "fetch-response",
201
- id,
202
- status: res.status,
203
- statusText: res.statusText,
204
- // Convert Headers to a plain object for structured clone
205
- headers: Object.fromEntries(res.headers.entries()),
206
- body
207
- },
208
- "*"
209
- );
210
- } catch (err) {
211
- iframeRef.current?.contentWindow?.postMessage(
212
- {
213
- source: "renderize",
214
- type: "fetch-response",
215
- id,
216
- error: err instanceof Error ? err.message : String(err)
217
- },
218
- "*"
219
- );
220
- }
221
- }
222
- if (event.data.type === "error" && onError) {
223
- onError(event.data.message);
224
- }
225
- };
226
- window.addEventListener("message", handler);
227
- return () => window.removeEventListener("message", handler);
228
- }, [onError]);
229
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
230
- "div",
231
- {
232
- className,
233
- style: { width, height, overflow: "hidden", ...style },
234
- children: srcDoc && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
235
- "iframe",
236
- {
237
- ref: iframeRef,
238
- srcDoc,
239
- title: "Renderize Sandbox",
240
- sandbox: "allow-scripts allow-forms allow-modals allow-popups allow-downloads",
241
- style: {
242
- width: "100%",
243
- height: "100%",
244
- border: "none",
245
- display: "block"
246
- }
247
- },
248
- srcDoc
249
- )
250
- }
251
- );
252
- }
253
- // Annotate the CommonJS export names for ESM import in node:
254
- 0 && (module.exports = {
255
- Renderize
256
- });
package/dist/index.d.cts DELETED
@@ -1,20 +0,0 @@
1
- import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import React from 'react';
3
-
4
- interface RenderizeProps {
5
- /** React code generated by the LLM. Must define a function component named App. */
6
- code: string;
7
- /** Height of the sandbox iframe. Defaults to "100%" */
8
- height?: string | number;
9
- /** Width of the sandbox iframe. Defaults to "100%" */
10
- width?: string | number;
11
- /** Custom class name for the wrapper element */
12
- className?: string;
13
- /** Custom inline styles for the wrapper element */
14
- style?: React.CSSProperties;
15
- /** Called when the sandbox encounters a runtime error */
16
- onError?: (error: string) => void;
17
- }
18
- declare function Renderize({ code, height, width, className, style, onError, }: RenderizeProps): react_jsx_runtime.JSX.Element;
19
-
20
- export { Renderize, type RenderizeProps };
package/dist/index.d.ts DELETED
@@ -1,20 +0,0 @@
1
- import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import React from 'react';
3
-
4
- interface RenderizeProps {
5
- /** React code generated by the LLM. Must define a function component named App. */
6
- code: string;
7
- /** Height of the sandbox iframe. Defaults to "100%" */
8
- height?: string | number;
9
- /** Width of the sandbox iframe. Defaults to "100%" */
10
- width?: string | number;
11
- /** Custom class name for the wrapper element */
12
- className?: string;
13
- /** Custom inline styles for the wrapper element */
14
- style?: React.CSSProperties;
15
- /** Called when the sandbox encounters a runtime error */
16
- onError?: (error: string) => void;
17
- }
18
- declare function Renderize({ code, height, width, className, style, onError, }: RenderizeProps): react_jsx_runtime.JSX.Element;
19
-
20
- export { Renderize, type RenderizeProps };
package/dist/index.js DELETED
@@ -1,229 +0,0 @@
1
- // src/Renderize.tsx
2
- import { useEffect, useRef, useState } from "react";
3
-
4
- // src/template.ts
5
- function buildTemplate(code) {
6
- return `<!DOCTYPE html>
7
- <html lang="en">
8
- <head>
9
- <meta charset="UTF-8" />
10
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
11
-
12
- <script src="https://cdn.tailwindcss.com"></script>
13
- <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
14
-
15
- <script type="importmap">
16
- {
17
- "imports": {
18
- "react": "https://esm.sh/react@18",
19
- "react/jsx-runtime": "https://esm.sh/react@18/jsx-runtime",
20
- "react-dom": "https://esm.sh/react-dom@18",
21
- "react-dom/client": "https://esm.sh/react-dom@18/client",
22
- "lucide-react": "https://esm.sh/lucide-react?external=react",
23
- "clsx": "https://esm.sh/clsx",
24
- "class-variance-authority": "https://esm.sh/class-variance-authority",
25
- "tailwind-merge": "https://esm.sh/tailwind-merge",
26
- "@radix-ui/react-accordion": "https://esm.sh/@radix-ui/react-accordion?external=react,react-dom",
27
- "@radix-ui/react-alert-dialog": "https://esm.sh/@radix-ui/react-alert-dialog?external=react,react-dom",
28
- "@radix-ui/react-avatar": "https://esm.sh/@radix-ui/react-avatar?external=react,react-dom",
29
- "@radix-ui/react-checkbox": "https://esm.sh/@radix-ui/react-checkbox?external=react,react-dom",
30
- "@radix-ui/react-collapsible": "https://esm.sh/@radix-ui/react-collapsible?external=react,react-dom",
31
- "@radix-ui/react-context-menu": "https://esm.sh/@radix-ui/react-context-menu?external=react,react-dom",
32
- "@radix-ui/react-dialog": "https://esm.sh/@radix-ui/react-dialog?external=react,react-dom",
33
- "@radix-ui/react-dropdown-menu": "https://esm.sh/@radix-ui/react-dropdown-menu?external=react,react-dom",
34
- "@radix-ui/react-hover-card": "https://esm.sh/@radix-ui/react-hover-card?external=react,react-dom",
35
- "@radix-ui/react-label": "https://esm.sh/@radix-ui/react-label?external=react,react-dom",
36
- "@radix-ui/react-menubar": "https://esm.sh/@radix-ui/react-menubar?external=react,react-dom",
37
- "@radix-ui/react-navigation-menu": "https://esm.sh/@radix-ui/react-navigation-menu?external=react,react-dom",
38
- "@radix-ui/react-popover": "https://esm.sh/@radix-ui/react-popover?external=react,react-dom",
39
- "@radix-ui/react-progress": "https://esm.sh/@radix-ui/react-progress?external=react,react-dom",
40
- "@radix-ui/react-radio-group": "https://esm.sh/@radix-ui/react-radio-group?external=react,react-dom",
41
- "@radix-ui/react-scroll-area": "https://esm.sh/@radix-ui/react-scroll-area?external=react,react-dom",
42
- "@radix-ui/react-select": "https://esm.sh/@radix-ui/react-select?external=react,react-dom",
43
- "@radix-ui/react-separator": "https://esm.sh/@radix-ui/react-separator?external=react,react-dom",
44
- "@radix-ui/react-slider": "https://esm.sh/@radix-ui/react-slider?external=react,react-dom",
45
- "@radix-ui/react-slot": "https://esm.sh/@radix-ui/react-slot?external=react,react-dom",
46
- "@radix-ui/react-switch": "https://esm.sh/@radix-ui/react-switch?external=react,react-dom",
47
- "@radix-ui/react-tabs": "https://esm.sh/@radix-ui/react-tabs?external=react,react-dom",
48
- "@radix-ui/react-toast": "https://esm.sh/@radix-ui/react-toast?external=react,react-dom",
49
- "@radix-ui/react-toggle": "https://esm.sh/@radix-ui/react-toggle?external=react,react-dom",
50
- "@radix-ui/react-toggle-group": "https://esm.sh/@radix-ui/react-toggle-group?external=react,react-dom",
51
- "@radix-ui/react-toolbar": "https://esm.sh/@radix-ui/react-toolbar?external=react,react-dom",
52
- "@radix-ui/react-tooltip": "https://esm.sh/@radix-ui/react-tooltip?external=react,react-dom"
53
- }
54
- }
55
- </script>
56
-
57
- <style>
58
- * { box-sizing: border-box; }
59
- body { margin: 0; padding: 0; }
60
- </style>
61
- </head>
62
- <body>
63
- <div id="root"></div>
64
-
65
- <script>
66
- // \u2500\u2500 FETCH PROXY \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
67
- // Override window.fetch to proxy requests through the parent window.
68
- // This solves the CORS/null-origin issue with srcdoc iframes:
69
- // the parent has a real origin and can make fetch calls freely.
70
- window.fetch = function(url, options = {}) {
71
- return new Promise((resolve, reject) => {
72
- const id = Math.random().toString(36).slice(2) + Date.now().toString(36);
73
-
74
- // Serialize body \u2014 postMessage can't transfer Request objects
75
- const serializedOptions = {
76
- method: options.method || "GET",
77
- headers: options.headers || {},
78
- body: options.body || null,
79
- };
80
-
81
- // Listen for the response from the parent
82
- function handleMessage(event) {
83
- if (
84
- event.data?.source !== "renderize" ||
85
- event.data?.type !== "fetch-response" ||
86
- event.data?.id !== id
87
- ) return;
88
-
89
- window.removeEventListener("message", handleMessage);
90
-
91
- if (event.data.error) {
92
- reject(new Error(event.data.error));
93
- return;
94
- }
95
-
96
- // Reconstruct a real Response object from the serialized data
97
- const { status, statusText, headers, body } = event.data;
98
- const responseBody = typeof body === "string" ? body : JSON.stringify(body);
99
-
100
- const response = new Response(responseBody, {
101
- status,
102
- statusText,
103
- headers: new Headers(headers),
104
- });
105
-
106
- resolve(response);
107
- }
108
-
109
- window.addEventListener("message", handleMessage);
110
-
111
- // Ask the parent to perform the fetch on our behalf
112
- window.parent.postMessage({
113
- source: "renderize",
114
- type: "fetch-request",
115
- id,
116
- url,
117
- options: serializedOptions,
118
- }, "*");
119
- });
120
- };
121
- // \u2500\u2500 END FETCH PROXY \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
122
- </script>
123
-
124
- <script type="text/babel" data-type="module">
125
- import React, {
126
- useState, useEffect, useRef, useCallback,
127
- useMemo, useReducer, useContext, createContext,
128
- forwardRef, Fragment
129
- } from "react";
130
- import { createRoot } from "react-dom/client";
131
-
132
- // \u2500\u2500 USER CODE START \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
133
- ${code}
134
- // \u2500\u2500 USER CODE END \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
135
-
136
- const container = document.getElementById("root");
137
- createRoot(container).render(React.createElement(App));
138
- </script>
139
- </body>
140
- </html>`;
141
- }
142
-
143
- // src/Renderize.tsx
144
- import { jsx } from "react/jsx-runtime";
145
- function Renderize({
146
- code,
147
- height = "100%",
148
- width = "100%",
149
- className,
150
- style,
151
- onError
152
- }) {
153
- const iframeRef = useRef(null);
154
- const [srcDoc, setSrcDoc] = useState(null);
155
- useEffect(() => {
156
- if (!code?.trim()) return;
157
- setSrcDoc(buildTemplate(code));
158
- }, [code]);
159
- useEffect(() => {
160
- const handler = async (event) => {
161
- if (event.data?.source !== "renderize") return;
162
- if (event.data.type === "fetch-request") {
163
- const { id, url, options } = event.data;
164
- try {
165
- const res = await fetch(url, {
166
- method: options.method,
167
- headers: options.headers,
168
- body: options.body ?? void 0
169
- });
170
- const body = await res.text();
171
- iframeRef.current?.contentWindow?.postMessage(
172
- {
173
- source: "renderize",
174
- type: "fetch-response",
175
- id,
176
- status: res.status,
177
- statusText: res.statusText,
178
- // Convert Headers to a plain object for structured clone
179
- headers: Object.fromEntries(res.headers.entries()),
180
- body
181
- },
182
- "*"
183
- );
184
- } catch (err) {
185
- iframeRef.current?.contentWindow?.postMessage(
186
- {
187
- source: "renderize",
188
- type: "fetch-response",
189
- id,
190
- error: err instanceof Error ? err.message : String(err)
191
- },
192
- "*"
193
- );
194
- }
195
- }
196
- if (event.data.type === "error" && onError) {
197
- onError(event.data.message);
198
- }
199
- };
200
- window.addEventListener("message", handler);
201
- return () => window.removeEventListener("message", handler);
202
- }, [onError]);
203
- return /* @__PURE__ */ jsx(
204
- "div",
205
- {
206
- className,
207
- style: { width, height, overflow: "hidden", ...style },
208
- children: srcDoc && /* @__PURE__ */ jsx(
209
- "iframe",
210
- {
211
- ref: iframeRef,
212
- srcDoc,
213
- title: "Renderize Sandbox",
214
- sandbox: "allow-scripts allow-forms allow-modals allow-popups allow-downloads",
215
- style: {
216
- width: "100%",
217
- height: "100%",
218
- border: "none",
219
- display: "block"
220
- }
221
- },
222
- srcDoc
223
- )
224
- }
225
- );
226
- }
227
- export {
228
- Renderize
229
- };