@aquiles-ai/renderize 1.6.0 → 1.8.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/dist/index.cjs CHANGED
@@ -88,8 +88,71 @@ function buildTemplate(code) {
88
88
  <body>
89
89
  <div id="root"></div>
90
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 = crypto.randomUUID();
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
+
91
150
  <script type="text/babel" data-type="module">
92
- import React, { useState, useEffect, useRef, useCallback, useMemo, useReducer, useContext, createContext } from "react";
151
+ import React, {
152
+ useState, useEffect, useRef, useCallback,
153
+ useMemo, useReducer, useContext, createContext,
154
+ forwardRef, Fragment
155
+ } from "react";
93
156
  import { createRoot } from "react-dom/client";
94
157
 
95
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
@@ -113,14 +176,50 @@ function Renderize({
113
176
  style,
114
177
  onError
115
178
  }) {
179
+ const iframeRef = (0, import_react.useRef)(null);
116
180
  const [srcDoc, setSrcDoc] = (0, import_react.useState)(null);
117
181
  (0, import_react.useEffect)(() => {
118
182
  if (!code?.trim()) return;
119
183
  setSrcDoc(buildTemplate(code));
120
184
  }, [code]);
121
185
  (0, import_react.useEffect)(() => {
122
- const handler = (event) => {
123
- if (event.data?.source === "renderize" && event.data?.type === "error" && onError) {
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) {
124
223
  onError(event.data.message);
125
224
  }
126
225
  };
@@ -135,9 +234,10 @@ function Renderize({
135
234
  children: srcDoc && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
136
235
  "iframe",
137
236
  {
237
+ ref: iframeRef,
138
238
  srcDoc,
139
239
  title: "Renderize Sandbox",
140
- sandbox: "allow-scripts",
240
+ sandbox: "allow-scripts allow-forms allow-modals allow-popups allow-downloads",
141
241
  style: {
142
242
  width: "100%",
143
243
  height: "100%",
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/Renderize.tsx
2
- import { useEffect, useState } from "react";
2
+ import { useEffect, useRef, useState } from "react";
3
3
 
4
4
  // src/template.ts
5
5
  function buildTemplate(code) {
@@ -62,8 +62,71 @@ function buildTemplate(code) {
62
62
  <body>
63
63
  <div id="root"></div>
64
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 = crypto.randomUUID();
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
+
65
124
  <script type="text/babel" data-type="module">
66
- import React, { useState, useEffect, useRef, useCallback, useMemo, useReducer, useContext, createContext } from "react";
125
+ import React, {
126
+ useState, useEffect, useRef, useCallback,
127
+ useMemo, useReducer, useContext, createContext,
128
+ forwardRef, Fragment
129
+ } from "react";
67
130
  import { createRoot } from "react-dom/client";
68
131
 
69
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
@@ -87,14 +150,50 @@ function Renderize({
87
150
  style,
88
151
  onError
89
152
  }) {
153
+ const iframeRef = useRef(null);
90
154
  const [srcDoc, setSrcDoc] = useState(null);
91
155
  useEffect(() => {
92
156
  if (!code?.trim()) return;
93
157
  setSrcDoc(buildTemplate(code));
94
158
  }, [code]);
95
159
  useEffect(() => {
96
- const handler = (event) => {
97
- if (event.data?.source === "renderize" && event.data?.type === "error" && onError) {
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) {
98
197
  onError(event.data.message);
99
198
  }
100
199
  };
@@ -109,9 +208,10 @@ function Renderize({
109
208
  children: srcDoc && /* @__PURE__ */ jsx(
110
209
  "iframe",
111
210
  {
211
+ ref: iframeRef,
112
212
  srcDoc,
113
213
  title: "Renderize Sandbox",
114
- sandbox: "allow-scripts",
214
+ sandbox: "allow-scripts allow-forms allow-modals allow-popups allow-downloads",
115
215
  style: {
116
216
  width: "100%",
117
217
  height: "100%",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aquiles-ai/renderize",
3
- "version": "1.6.0",
3
+ "version": "1.8.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,5 +1,5 @@
1
1
  /// <reference lib="dom" />
2
- import React, { useEffect, useState } from "react";
2
+ import React, { useEffect, useRef, useState } from "react";
3
3
  import { buildTemplate } from "./template.js";
4
4
 
5
5
  export interface RenderizeProps {
@@ -25,26 +25,65 @@ export function Renderize({
25
25
  style,
26
26
  onError,
27
27
  }: RenderizeProps) {
28
+ const iframeRef = useRef<HTMLIFrameElement>(null);
28
29
  const [srcDoc, setSrcDoc] = useState<string | null>(null);
29
30
 
30
31
  useEffect(() => {
31
32
  if (!code?.trim()) return;
32
- // srcdoc works with sandbox="allow-scripts" — no blob URL needed,
33
- // no same-origin restriction, content is inlined directly
34
33
  setSrcDoc(buildTemplate(code));
35
34
  }, [code]);
36
35
 
37
- // Forward runtime errors from the iframe to the parent
36
+ // Central message handler handles both fetch proxying and error forwarding
38
37
  useEffect(() => {
39
- const handler = (event: MessageEvent) => {
40
- if (
41
- event.data?.source === "renderize" &&
42
- event.data?.type === "error" &&
43
- onError
44
- ) {
38
+ const handler = async (event: MessageEvent) => {
39
+ if (event.data?.source !== "renderize") return;
40
+
41
+ // ── Fetch proxy ────────────────────────────────────────────────
42
+ if (event.data.type === "fetch-request") {
43
+ const { id, url, options } = event.data;
44
+
45
+ try {
46
+ const res = await fetch(url, {
47
+ method: options.method,
48
+ headers: options.headers,
49
+ body: options.body ?? undefined,
50
+ });
51
+
52
+ // Serialize the response body as text — the iframe will parse it
53
+ const body = await res.text();
54
+
55
+ iframeRef.current?.contentWindow?.postMessage(
56
+ {
57
+ source: "renderize",
58
+ type: "fetch-response",
59
+ id,
60
+ status: res.status,
61
+ statusText: res.statusText,
62
+ // Convert Headers to a plain object for structured clone
63
+ headers: Object.fromEntries(res.headers.entries()),
64
+ body,
65
+ },
66
+ "*"
67
+ );
68
+ } catch (err) {
69
+ iframeRef.current?.contentWindow?.postMessage(
70
+ {
71
+ source: "renderize",
72
+ type: "fetch-response",
73
+ id,
74
+ error: err instanceof Error ? err.message : String(err),
75
+ },
76
+ "*"
77
+ );
78
+ }
79
+ }
80
+
81
+ // ── Error forwarding ───────────────────────────────────────────
82
+ if (event.data.type === "error" && onError) {
45
83
  onError(event.data.message);
46
84
  }
47
85
  };
86
+
48
87
  window.addEventListener("message", handler);
49
88
  return () => window.removeEventListener("message", handler);
50
89
  }, [onError]);
@@ -56,12 +95,11 @@ export function Renderize({
56
95
  >
57
96
  {srcDoc && (
58
97
  <iframe
59
- // key forces a full remount on every new render,
60
- // guaranteeing a clean JS context each time
61
98
  key={srcDoc}
99
+ ref={iframeRef}
62
100
  srcDoc={srcDoc}
63
101
  title="Renderize Sandbox"
64
- sandbox="allow-scripts"
102
+ sandbox="allow-scripts allow-forms allow-modals allow-popups allow-downloads"
65
103
  style={{
66
104
  width: "100%",
67
105
  height: "100%",
package/src/template.ts CHANGED
@@ -58,8 +58,71 @@ export function buildTemplate(code: string): string {
58
58
  <body>
59
59
  <div id="root"><\/div>
60
60
 
61
+ <script>
62
+ // ── FETCH PROXY ──────────────────────────────────────────────────
63
+ // Override window.fetch to proxy requests through the parent window.
64
+ // This solves the CORS/null-origin issue with srcdoc iframes:
65
+ // the parent has a real origin and can make fetch calls freely.
66
+ window.fetch = function(url, options = {}) {
67
+ return new Promise((resolve, reject) => {
68
+ const id = crypto.randomUUID();
69
+
70
+ // Serialize body — postMessage can't transfer Request objects
71
+ const serializedOptions = {
72
+ method: options.method || "GET",
73
+ headers: options.headers || {},
74
+ body: options.body || null,
75
+ };
76
+
77
+ // Listen for the response from the parent
78
+ function handleMessage(event) {
79
+ if (
80
+ event.data?.source !== "renderize" ||
81
+ event.data?.type !== "fetch-response" ||
82
+ event.data?.id !== id
83
+ ) return;
84
+
85
+ window.removeEventListener("message", handleMessage);
86
+
87
+ if (event.data.error) {
88
+ reject(new Error(event.data.error));
89
+ return;
90
+ }
91
+
92
+ // Reconstruct a real Response object from the serialized data
93
+ const { status, statusText, headers, body } = event.data;
94
+ const responseBody = typeof body === "string" ? body : JSON.stringify(body);
95
+
96
+ const response = new Response(responseBody, {
97
+ status,
98
+ statusText,
99
+ headers: new Headers(headers),
100
+ });
101
+
102
+ resolve(response);
103
+ }
104
+
105
+ window.addEventListener("message", handleMessage);
106
+
107
+ // Ask the parent to perform the fetch on our behalf
108
+ window.parent.postMessage({
109
+ source: "renderize",
110
+ type: "fetch-request",
111
+ id,
112
+ url,
113
+ options: serializedOptions,
114
+ }, "*");
115
+ });
116
+ };
117
+ // ── END FETCH PROXY ──────────────────────────────────────────────
118
+ <\/script>
119
+
61
120
  <script type="text/babel" data-type="module">
62
- import React, { useState, useEffect, useRef, useCallback, useMemo, useReducer, useContext, createContext } from "react";
121
+ import React, {
122
+ useState, useEffect, useRef, useCallback,
123
+ useMemo, useReducer, useContext, createContext,
124
+ forwardRef, Fragment
125
+ } from "react";
63
126
  import { createRoot } from "react-dom/client";
64
127
 
65
128
  // ── USER CODE START ──────────────────────────────────────────────