@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 +146 -0
- package/package.json +1 -1
- package/src/Renderize.tsx +2 -1
- package/src/sanitize.ts +297 -0
- package/src/template.ts +22 -0
- package/dist/index.cjs +0 -256
- package/dist/index.d.cts +0 -20
- package/dist/index.d.ts +0 -20
- package/dist/index.js +0 -229
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
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
|
package/src/sanitize.ts
ADDED
|
@@ -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
|
-
};
|