@illuma-ai/code-sandbox 1.3.0 → 1.3.1

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/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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@illuma-ai/code-sandbox",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "type": "module",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Illuma AI (https://github.com/illuma-ai)",
@@ -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({ url, className = "", onBrowserError }: PreviewProps) {
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 ? (
@@ -183,6 +183,7 @@ export const CodeSandbox = forwardRef<CodeSandboxHandle, CodeSandboxProps>(
183
183
  <Preview
184
184
  url={state.previewUrl}
185
185
  onBrowserError={handleBrowserError}
186
+ reloadKey={state.previewReloadKey}
186
187
  />
187
188
  </motion.div>
188
189
  </div>
@@ -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
- // Update React state with new file set + diff info
360
- setState((prev) => ({
361
- ...prev,
362
- originalFiles: currentFiles,
363
- files: newFiles,
364
- fileChanges,
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
- console.log(
370
- DBG,
371
- `files changed — restarting server (${Object.keys(fileChanges).length} files differ)`,
372
- );
373
- try {
374
- await runtime.restart();
375
- } catch (err) {
376
- console.error(DBG, "restart failed:", err);
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 {