@illuma-ai/code-sandbox 1.3.0 → 1.4.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 +715 -0
- package/dist/components/Preview.d.ts +1 -1
- package/dist/index.cjs +32 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +3188 -3129
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +12 -0
- package/package.json +1 -1
- package/src/components/Preview.tsx +30 -1
- package/src/components/Workbench.tsx +1 -0
- package/src/hooks/useRuntime.ts +80 -16
- package/src/types.ts +12 -0
package/dist/types.d.ts
CHANGED
|
@@ -114,6 +114,12 @@ export interface RuntimeState {
|
|
|
114
114
|
* Missing keys should be treated as "unchanged".
|
|
115
115
|
*/
|
|
116
116
|
fileChanges: Record<string, FileChangeStatus>;
|
|
117
|
+
/**
|
|
118
|
+
* Monotonically increasing counter that triggers an iframe reload
|
|
119
|
+
* without restarting the server process. Incremented when only
|
|
120
|
+
* hot-reloadable files change (CSS, HTML, static assets).
|
|
121
|
+
*/
|
|
122
|
+
previewReloadKey: number;
|
|
117
123
|
/** Error message if status is 'error' */
|
|
118
124
|
error: string | null;
|
|
119
125
|
/**
|
|
@@ -293,6 +299,12 @@ export interface PreviewProps {
|
|
|
293
299
|
onRefresh?: () => void;
|
|
294
300
|
/** Called when a JavaScript error occurs inside the preview iframe */
|
|
295
301
|
onBrowserError?: (error: SandboxError) => void;
|
|
302
|
+
/**
|
|
303
|
+
* Monotonically increasing counter. When this changes, the iframe
|
|
304
|
+
* is soft-reloaded (location.reload) without restarting the server.
|
|
305
|
+
* Used for hot-reloading static asset changes (CSS, HTML, images).
|
|
306
|
+
*/
|
|
307
|
+
reloadKey?: number;
|
|
296
308
|
}
|
|
297
309
|
export interface BootOverlayProps {
|
|
298
310
|
progress: BootProgress;
|
package/package.json
CHANGED
|
@@ -123,11 +123,22 @@ let browserErrorCounter = 0;
|
|
|
123
123
|
* After the iframe loads, an error-capture script is injected so that
|
|
124
124
|
* runtime errors inside the preview are surfaced to the AI agent.
|
|
125
125
|
*/
|
|
126
|
-
export function Preview({
|
|
126
|
+
export function Preview({
|
|
127
|
+
url,
|
|
128
|
+
className = "",
|
|
129
|
+
onBrowserError,
|
|
130
|
+
reloadKey = 0,
|
|
131
|
+
}: PreviewProps) {
|
|
127
132
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
128
133
|
const onBrowserErrorRef = useRef(onBrowserError);
|
|
129
134
|
onBrowserErrorRef.current = onBrowserError;
|
|
130
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Track the previous reloadKey so we only reload on actual changes,
|
|
138
|
+
* not on initial mount or unrelated re-renders.
|
|
139
|
+
*/
|
|
140
|
+
const prevReloadKeyRef = useRef(reloadKey);
|
|
141
|
+
|
|
131
142
|
/**
|
|
132
143
|
* Inject the error-capture script into the iframe's contentWindow.
|
|
133
144
|
*
|
|
@@ -207,6 +218,24 @@ export function Preview({ url, className = "", onBrowserError }: PreviewProps) {
|
|
|
207
218
|
}
|
|
208
219
|
}, [url]);
|
|
209
220
|
|
|
221
|
+
// Hot reload: soft-refresh the iframe when reloadKey changes.
|
|
222
|
+
// This avoids a full server restart for static asset changes (CSS, HTML).
|
|
223
|
+
useEffect(() => {
|
|
224
|
+
if (reloadKey !== prevReloadKeyRef.current) {
|
|
225
|
+
prevReloadKeyRef.current = reloadKey;
|
|
226
|
+
const iframe = iframeRef.current;
|
|
227
|
+
if (iframe && url) {
|
|
228
|
+
try {
|
|
229
|
+
// Try same-origin reload first (preserves scroll position)
|
|
230
|
+
iframe.contentWindow?.location.reload();
|
|
231
|
+
} catch {
|
|
232
|
+
// Cross-origin fallback — re-assign src
|
|
233
|
+
iframe.src = url;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}, [reloadKey, url]);
|
|
238
|
+
|
|
210
239
|
return (
|
|
211
240
|
<div className={`h-full w-full bg-white ${className}`}>
|
|
212
241
|
{url ? (
|
package/src/hooks/useRuntime.ts
CHANGED
|
@@ -29,6 +29,38 @@ import type {
|
|
|
29
29
|
/** Debug log prefix for easy filtering in DevTools */
|
|
30
30
|
const DBG = "[CodeSandbox:useRuntime]";
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* File extensions that can be hot-reloaded (iframe refresh) without
|
|
34
|
+
* restarting the Node.js server process. These are static assets
|
|
35
|
+
* served by Express — changing them doesn't require a process restart.
|
|
36
|
+
*/
|
|
37
|
+
const HOT_RELOAD_EXTENSIONS = new Set([
|
|
38
|
+
"css",
|
|
39
|
+
"html",
|
|
40
|
+
"htm",
|
|
41
|
+
"svg",
|
|
42
|
+
"png",
|
|
43
|
+
"jpg",
|
|
44
|
+
"jpeg",
|
|
45
|
+
"gif",
|
|
46
|
+
"webp",
|
|
47
|
+
"ico",
|
|
48
|
+
"woff",
|
|
49
|
+
"woff2",
|
|
50
|
+
"ttf",
|
|
51
|
+
"eot",
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Check whether a file path is hot-reloadable (static asset that doesn't
|
|
56
|
+
* require a server restart). Returns false for JS/TS/JSON and any file
|
|
57
|
+
* without an extension.
|
|
58
|
+
*/
|
|
59
|
+
function isHotReloadable(filePath: string): boolean {
|
|
60
|
+
const ext = filePath.split(".").pop()?.toLowerCase();
|
|
61
|
+
return ext ? HOT_RELOAD_EXTENSIONS.has(ext) : false;
|
|
62
|
+
}
|
|
63
|
+
|
|
32
64
|
/**
|
|
33
65
|
* Compute per-file change statuses between an original and current file set.
|
|
34
66
|
*
|
|
@@ -115,6 +147,7 @@ export function useRuntime(props: CodeSandboxProps) {
|
|
|
115
147
|
files: {},
|
|
116
148
|
originalFiles: {},
|
|
117
149
|
fileChanges: {},
|
|
150
|
+
previewReloadKey: 0,
|
|
118
151
|
error: null,
|
|
119
152
|
errors: [],
|
|
120
153
|
});
|
|
@@ -340,12 +373,14 @@ export function useRuntime(props: CodeSandboxProps) {
|
|
|
340
373
|
// Runtime exists — diff and write changed files
|
|
341
374
|
const currentFiles = runtime.getCurrentFiles();
|
|
342
375
|
let changed = false;
|
|
376
|
+
const changedPaths: string[] = [];
|
|
343
377
|
|
|
344
378
|
for (const [path, content] of Object.entries(newFiles)) {
|
|
345
379
|
if (currentFiles[path] !== content) {
|
|
346
380
|
try {
|
|
347
381
|
await runtime.writeFile(path, content);
|
|
348
382
|
changed = true;
|
|
383
|
+
changedPaths.push(path);
|
|
349
384
|
console.log(DBG, `updated file: ${path}`);
|
|
350
385
|
} catch (err) {
|
|
351
386
|
console.warn(DBG, `failed to write ${path}:`, err);
|
|
@@ -356,25 +391,54 @@ export function useRuntime(props: CodeSandboxProps) {
|
|
|
356
391
|
// Compute change status — "original" is what we had before this update
|
|
357
392
|
const fileChanges = computeFileChanges(currentFiles, newFiles);
|
|
358
393
|
|
|
359
|
-
//
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
394
|
+
// Decide: hot reload (iframe refresh) vs cold restart (kill + rerun server).
|
|
395
|
+
// Hot reload when ALL changed files are static assets (CSS, HTML, images).
|
|
396
|
+
// Cold restart when ANY changed file is JS/TS/JSON/etc. that affects the server.
|
|
397
|
+
const canHotReload =
|
|
398
|
+
changed &&
|
|
399
|
+
changedPaths.length > 0 &&
|
|
400
|
+
changedPaths.every(isHotReloadable);
|
|
366
401
|
|
|
367
|
-
// Restart server if any files changed
|
|
368
402
|
if (changed && shouldRestart) {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
403
|
+
if (canHotReload) {
|
|
404
|
+
// Hot reload — just bump the preview reload key to refresh the iframe
|
|
405
|
+
console.log(
|
|
406
|
+
DBG,
|
|
407
|
+
`hot reload: ${changedPaths.length} static file(s) changed — refreshing preview`,
|
|
408
|
+
);
|
|
409
|
+
setState((prev) => ({
|
|
410
|
+
...prev,
|
|
411
|
+
originalFiles: currentFiles,
|
|
412
|
+
files: newFiles,
|
|
413
|
+
fileChanges,
|
|
414
|
+
previewReloadKey: prev.previewReloadKey + 1,
|
|
415
|
+
}));
|
|
416
|
+
} else {
|
|
417
|
+
// Cold restart — server code changed, need full process restart
|
|
418
|
+
console.log(
|
|
419
|
+
DBG,
|
|
420
|
+
`cold restart: ${changedPaths.length} file(s) changed — restarting server`,
|
|
421
|
+
);
|
|
422
|
+
setState((prev) => ({
|
|
423
|
+
...prev,
|
|
424
|
+
originalFiles: currentFiles,
|
|
425
|
+
files: newFiles,
|
|
426
|
+
fileChanges,
|
|
427
|
+
}));
|
|
428
|
+
try {
|
|
429
|
+
await runtime.restart();
|
|
430
|
+
} catch (err) {
|
|
431
|
+
console.error(DBG, "restart failed:", err);
|
|
432
|
+
}
|
|
377
433
|
}
|
|
434
|
+
} else {
|
|
435
|
+
// No changes or restart disabled — just update state
|
|
436
|
+
setState((prev) => ({
|
|
437
|
+
...prev,
|
|
438
|
+
originalFiles: currentFiles,
|
|
439
|
+
files: newFiles,
|
|
440
|
+
fileChanges,
|
|
441
|
+
}));
|
|
378
442
|
}
|
|
379
443
|
|
|
380
444
|
// Notify host that files have been processed
|
package/src/types.ts
CHANGED
|
@@ -150,6 +150,12 @@ export interface RuntimeState {
|
|
|
150
150
|
* Missing keys should be treated as "unchanged".
|
|
151
151
|
*/
|
|
152
152
|
fileChanges: Record<string, FileChangeStatus>;
|
|
153
|
+
/**
|
|
154
|
+
* Monotonically increasing counter that triggers an iframe reload
|
|
155
|
+
* without restarting the server process. Incremented when only
|
|
156
|
+
* hot-reloadable files change (CSS, HTML, static assets).
|
|
157
|
+
*/
|
|
158
|
+
previewReloadKey: number;
|
|
153
159
|
/** Error message if status is 'error' */
|
|
154
160
|
error: string | null;
|
|
155
161
|
/**
|
|
@@ -355,6 +361,12 @@ export interface PreviewProps {
|
|
|
355
361
|
onRefresh?: () => void;
|
|
356
362
|
/** Called when a JavaScript error occurs inside the preview iframe */
|
|
357
363
|
onBrowserError?: (error: SandboxError) => void;
|
|
364
|
+
/**
|
|
365
|
+
* Monotonically increasing counter. When this changes, the iframe
|
|
366
|
+
* is soft-reloaded (location.reload) without restarting the server.
|
|
367
|
+
* Used for hot-reloading static asset changes (CSS, HTML, images).
|
|
368
|
+
*/
|
|
369
|
+
reloadKey?: number;
|
|
358
370
|
}
|
|
359
371
|
|
|
360
372
|
export interface BootOverlayProps {
|