@illuma-ai/code-sandbox 1.1.0 → 1.2.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/components/Workbench.d.ts +3 -2
- package/dist/hooks/useRuntime.d.ts +24 -14
- package/dist/index.cjs +81 -85
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.js +6121 -6364
- package/dist/index.js.map +1 -1
- package/dist/services/runtime.d.ts +1 -1
- package/dist/types.d.ts +104 -41
- package/package.json +1 -1
- package/src/components/Workbench.tsx +133 -87
- package/src/hooks/useRuntime.ts +182 -216
- package/src/index.ts +1 -5
- package/src/services/runtime.ts +1 -1
- package/src/types.ts +110 -42
- package/dist/services/git.d.ts +0 -57
- package/src/services/git.ts +0 -415
|
@@ -24,7 +24,7 @@ type ErrorCallback = (error: SandboxError) => void;
|
|
|
24
24
|
* - Write project files into the virtual filesystem
|
|
25
25
|
* - Install npm dependencies from package.json
|
|
26
26
|
* - Start the entry command (e.g., "node server.js")
|
|
27
|
-
* - Track file modifications for
|
|
27
|
+
* - Track file modifications for diffing
|
|
28
28
|
* - Provide preview URL for the iframe
|
|
29
29
|
*/
|
|
30
30
|
export declare class NodepodRuntime {
|
package/dist/types.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* @illuma-ai/code-sandbox — Type definitions
|
|
3
3
|
*
|
|
4
4
|
* Core types for the browser-native code sandbox.
|
|
5
|
-
* Covers file maps, runtime states,
|
|
5
|
+
* Covers file maps, runtime states, imperative handle, and component props.
|
|
6
6
|
*/
|
|
7
7
|
/** A flat map of file paths to their string contents */
|
|
8
8
|
export type FileMap = Record<string, string>;
|
|
@@ -102,11 +102,9 @@ export interface RuntimeState {
|
|
|
102
102
|
files: FileMap;
|
|
103
103
|
/**
|
|
104
104
|
* Baseline file snapshot — the files as they were before the latest
|
|
105
|
-
* update (
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
* (everything is "new"), populated after the first file update
|
|
109
|
-
* (GitHub poll, updateFiles, etc.).
|
|
105
|
+
* update (via updateFiles). Used by the diff viewer to show what changed.
|
|
106
|
+
* Empty on first load (everything is "new"), populated after the first
|
|
107
|
+
* file update.
|
|
110
108
|
*/
|
|
111
109
|
originalFiles: FileMap;
|
|
112
110
|
/**
|
|
@@ -125,42 +123,101 @@ export interface RuntimeState {
|
|
|
125
123
|
*/
|
|
126
124
|
errors: SandboxError[];
|
|
127
125
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
126
|
+
/**
|
|
127
|
+
* Imperative handle exposed by CodeSandbox via React.forwardRef.
|
|
128
|
+
*
|
|
129
|
+
* This is the primary API for host applications (like Ranger) to
|
|
130
|
+
* control the sandbox programmatically. The sandbox is a "dumb renderer"
|
|
131
|
+
* — the host pushes files in and reads state out.
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* ```tsx
|
|
135
|
+
* const ref = useRef<CodeSandboxHandle>(null);
|
|
136
|
+
*
|
|
137
|
+
* // Push files from any source
|
|
138
|
+
* ref.current?.updateFiles(fileMap);
|
|
139
|
+
*
|
|
140
|
+
* // Push a single file (e.g., agent modified one file)
|
|
141
|
+
* ref.current?.updateFile('server.js', newContent);
|
|
142
|
+
*
|
|
143
|
+
* // Read current state
|
|
144
|
+
* const files = ref.current?.getFiles();
|
|
145
|
+
* const changes = ref.current?.getChangedFiles();
|
|
146
|
+
* const errors = ref.current?.getErrors();
|
|
147
|
+
* ```
|
|
148
|
+
*/
|
|
149
|
+
export interface CodeSandboxHandle {
|
|
150
|
+
/**
|
|
151
|
+
* Replace the entire file set. Diffs against current files, writes only
|
|
152
|
+
* changed files to Nodepod FS, and restarts the server if anything changed.
|
|
153
|
+
*
|
|
154
|
+
* The previous file set becomes `originalFiles` for diff tracking.
|
|
155
|
+
*
|
|
156
|
+
* @param files - Complete new file set
|
|
157
|
+
* @param options - Optional: restartServer (default true)
|
|
158
|
+
*/
|
|
159
|
+
updateFiles: (files: FileMap, options?: {
|
|
160
|
+
restartServer?: boolean;
|
|
161
|
+
}) => Promise<void>;
|
|
162
|
+
/**
|
|
163
|
+
* Update a single file. Writes to Nodepod FS and updates state.
|
|
164
|
+
* Does NOT restart the server — call `restart()` manually if needed,
|
|
165
|
+
* or use `updateFiles()` for bulk updates with auto-restart.
|
|
166
|
+
*
|
|
167
|
+
* @param path - File path (e.g., "server.js", "public/index.html")
|
|
168
|
+
* @param content - New file content
|
|
169
|
+
*/
|
|
170
|
+
updateFile: (path: string, content: string) => Promise<void>;
|
|
171
|
+
/**
|
|
172
|
+
* Force restart the server process. Kills the current process
|
|
173
|
+
* and re-runs the entry command.
|
|
174
|
+
*/
|
|
175
|
+
restart: () => Promise<void>;
|
|
176
|
+
/**
|
|
177
|
+
* Get the current file map (all files including edits).
|
|
178
|
+
*/
|
|
179
|
+
getFiles: () => FileMap;
|
|
180
|
+
/**
|
|
181
|
+
* Get only files that have been modified relative to originalFiles.
|
|
182
|
+
* Returns a FileMap containing only the changed files.
|
|
183
|
+
*/
|
|
184
|
+
getChangedFiles: () => FileMap;
|
|
185
|
+
/**
|
|
186
|
+
* Get the per-file change status map (new/modified/deleted).
|
|
187
|
+
* Missing keys should be treated as "unchanged".
|
|
188
|
+
*/
|
|
189
|
+
getFileChanges: () => Record<string, FileChangeStatus>;
|
|
190
|
+
/**
|
|
191
|
+
* Get all structured errors collected during this session.
|
|
192
|
+
*/
|
|
193
|
+
getErrors: () => SandboxError[];
|
|
194
|
+
/**
|
|
195
|
+
* Get the current runtime state snapshot.
|
|
196
|
+
*/
|
|
197
|
+
getState: () => RuntimeState;
|
|
156
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* Props for the CodeSandbox component.
|
|
201
|
+
*
|
|
202
|
+
* The sandbox is a pure renderer — it receives files via props and
|
|
203
|
+
* exposes an imperative handle for programmatic updates. It does NOT
|
|
204
|
+
* interact with any storage backend directly.
|
|
205
|
+
*
|
|
206
|
+
* File sources (for initial load, pick ONE):
|
|
207
|
+
* - `files` — pass a FileMap directly
|
|
208
|
+
* - `template` — use a built-in template name (e.g., "fullstack-starter")
|
|
209
|
+
*
|
|
210
|
+
* For live updates after mount, use the imperative handle:
|
|
211
|
+
* ```tsx
|
|
212
|
+
* const ref = useRef<CodeSandboxHandle>(null);
|
|
213
|
+
* <CodeSandbox ref={ref} files={initialFiles} />
|
|
214
|
+
* // Later:
|
|
215
|
+
* ref.current.updateFiles(newFiles);
|
|
216
|
+
* ```
|
|
217
|
+
*/
|
|
157
218
|
export interface CodeSandboxProps {
|
|
158
|
-
/** Pass files directly */
|
|
219
|
+
/** Pass files directly (from any source) */
|
|
159
220
|
files?: FileMap;
|
|
160
|
-
/** OR load from GitHub */
|
|
161
|
-
github?: GitHubRepo;
|
|
162
|
-
/** GitHub personal access token (for private repos and write-back) */
|
|
163
|
-
gitToken?: string;
|
|
164
221
|
/** OR use a built-in template name */
|
|
165
222
|
template?: string;
|
|
166
223
|
/** Command to start the dev server (default inferred from package.json or template) */
|
|
@@ -169,9 +226,9 @@ export interface CodeSandboxProps {
|
|
|
169
226
|
port?: number;
|
|
170
227
|
/** Environment variables */
|
|
171
228
|
env?: Record<string, string>;
|
|
172
|
-
/** Fires when a file is modified in the editor */
|
|
229
|
+
/** Fires when a file is modified in the editor by the user */
|
|
173
230
|
onFileChange?: (path: string, content: string) => void;
|
|
174
|
-
/** Fires when the dev server is ready */
|
|
231
|
+
/** Fires when the dev server is ready (initial boot or after restart) */
|
|
175
232
|
onServerReady?: (port: number, url: string) => void;
|
|
176
233
|
/** Fires on boot progress changes */
|
|
177
234
|
onProgress?: (progress: BootProgress) => void;
|
|
@@ -187,6 +244,12 @@ export interface CodeSandboxProps {
|
|
|
187
244
|
* Max 2 auto-fix attempts per error is recommended (see Ranger pattern).
|
|
188
245
|
*/
|
|
189
246
|
onSandboxError?: (error: SandboxError) => void;
|
|
247
|
+
/**
|
|
248
|
+
* Fires after updateFiles() completes (files written + server restarted).
|
|
249
|
+
* Useful for the host to know when the sandbox has finished processing
|
|
250
|
+
* a file push.
|
|
251
|
+
*/
|
|
252
|
+
onFilesUpdated?: (fileChanges: Record<string, FileChangeStatus>) => void;
|
|
190
253
|
/** CSS class name for the root element */
|
|
191
254
|
className?: string;
|
|
192
255
|
/** Height of the sandbox (default: '100vh') */
|
package/package.json
CHANGED
|
@@ -20,6 +20,8 @@ import React, {
|
|
|
20
20
|
useEffect,
|
|
21
21
|
useCallback,
|
|
22
22
|
useRef,
|
|
23
|
+
useImperativeHandle,
|
|
24
|
+
forwardRef,
|
|
23
25
|
} from "react";
|
|
24
26
|
import { Allotment } from "allotment";
|
|
25
27
|
import { motion, AnimatePresence } from "framer-motion";
|
|
@@ -32,7 +34,11 @@ import { Preview } from "./Preview";
|
|
|
32
34
|
import { BootOverlay } from "./BootOverlay";
|
|
33
35
|
import { ViewSlider, type WorkbenchView } from "./ViewSlider";
|
|
34
36
|
import { useRuntime } from "../hooks/useRuntime";
|
|
35
|
-
import type {
|
|
37
|
+
import type {
|
|
38
|
+
CodeSandboxProps,
|
|
39
|
+
CodeSandboxHandle,
|
|
40
|
+
FileChangeStatus,
|
|
41
|
+
} from "../types";
|
|
36
42
|
|
|
37
43
|
/** Transition for view crossfade */
|
|
38
44
|
const VIEW_TRANSITION = {
|
|
@@ -50,102 +56,142 @@ const VIEW_TRANSITION = {
|
|
|
50
56
|
* A compact toolbar row contains the view toggle and (in preview mode)
|
|
51
57
|
* a URL bar with refresh button.
|
|
52
58
|
*/
|
|
53
|
-
export
|
|
54
|
-
|
|
59
|
+
export const CodeSandbox = forwardRef<CodeSandboxHandle, CodeSandboxProps>(
|
|
60
|
+
function CodeSandbox(props, ref) {
|
|
61
|
+
const { className = "", height = "100vh", port = 3000 } = props;
|
|
55
62
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
const runtime = useRuntime(props);
|
|
64
|
+
const {
|
|
65
|
+
state,
|
|
66
|
+
selectedFile,
|
|
67
|
+
openFiles,
|
|
68
|
+
handleFileChange,
|
|
69
|
+
handleSelectFile,
|
|
70
|
+
handleCloseFile,
|
|
71
|
+
handleBrowserError,
|
|
72
|
+
restart,
|
|
73
|
+
updateFiles,
|
|
74
|
+
updateFile,
|
|
75
|
+
getFiles,
|
|
76
|
+
getChangedFiles,
|
|
77
|
+
getFileChanges,
|
|
78
|
+
getErrors,
|
|
79
|
+
getState,
|
|
80
|
+
} = runtime;
|
|
66
81
|
|
|
67
|
-
|
|
68
|
-
|
|
82
|
+
// Expose imperative handle to parent via ref
|
|
83
|
+
useImperativeHandle(
|
|
84
|
+
ref,
|
|
85
|
+
() => ({
|
|
86
|
+
updateFiles,
|
|
87
|
+
updateFile,
|
|
88
|
+
restart,
|
|
89
|
+
getFiles,
|
|
90
|
+
getChangedFiles,
|
|
91
|
+
getFileChanges,
|
|
92
|
+
getErrors,
|
|
93
|
+
getState,
|
|
94
|
+
}),
|
|
95
|
+
[
|
|
96
|
+
updateFiles,
|
|
97
|
+
updateFile,
|
|
98
|
+
restart,
|
|
99
|
+
getFiles,
|
|
100
|
+
getChangedFiles,
|
|
101
|
+
getFileChanges,
|
|
102
|
+
getErrors,
|
|
103
|
+
getState,
|
|
104
|
+
],
|
|
105
|
+
);
|
|
69
106
|
|
|
70
|
-
|
|
71
|
-
|
|
107
|
+
const fileTree = useMemo(() => buildFileTree(state.files), [state.files]);
|
|
108
|
+
const isBooting = state.status !== "ready" && state.status !== "error";
|
|
72
109
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
useEffect(() => {
|
|
76
|
-
if (hasPreview) {
|
|
77
|
-
setActiveView("preview");
|
|
78
|
-
}
|
|
79
|
-
}, [hasPreview]);
|
|
110
|
+
// Active view: code or preview
|
|
111
|
+
const [activeView, setActiveView] = useState<WorkbenchView>("code");
|
|
80
112
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
[
|
|
88
|
-
);
|
|
113
|
+
// Auto-switch to preview when server becomes ready
|
|
114
|
+
const hasPreview = !!state.previewUrl;
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
if (hasPreview) {
|
|
117
|
+
setActiveView("preview");
|
|
118
|
+
}
|
|
119
|
+
}, [hasPreview]);
|
|
89
120
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
121
|
+
// Switch to code view when a file is selected from the tree
|
|
122
|
+
const onFileSelect = useCallback(
|
|
123
|
+
(path: string) => {
|
|
124
|
+
handleSelectFile(path);
|
|
125
|
+
setActiveView("code");
|
|
126
|
+
},
|
|
127
|
+
[handleSelectFile],
|
|
128
|
+
);
|
|
97
129
|
|
|
98
|
-
|
|
99
|
-
<
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
/>
|
|
130
|
+
return (
|
|
131
|
+
<div
|
|
132
|
+
className={`sb-root relative w-full bg-sb-bg text-sb-text overflow-hidden flex flex-col ${className}`}
|
|
133
|
+
style={{ height }}
|
|
134
|
+
>
|
|
135
|
+
{/* Boot overlay */}
|
|
136
|
+
{isBooting && <BootOverlay progress={state.progress} />}
|
|
106
137
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
pointerEvents: activeView === "code" ? "auto" : "none",
|
|
116
|
-
}}
|
|
117
|
-
transition={{ duration: 0.15 }}
|
|
118
|
-
>
|
|
119
|
-
<CodeView
|
|
120
|
-
fileTree={fileTree}
|
|
121
|
-
files={state.files}
|
|
122
|
-
originalFiles={state.originalFiles}
|
|
123
|
-
fileChanges={state.fileChanges}
|
|
124
|
-
selectedFile={selectedFile}
|
|
125
|
-
openFiles={openFiles}
|
|
126
|
-
terminalOutput={state.terminalOutput}
|
|
127
|
-
onSelectFile={onFileSelect}
|
|
128
|
-
onCloseFile={handleCloseFile}
|
|
129
|
-
onFileChange={handleFileChange}
|
|
130
|
-
/>
|
|
131
|
-
</motion.div>
|
|
138
|
+
{/* Toolbar — compact row with view toggle + URL bar */}
|
|
139
|
+
<Toolbar
|
|
140
|
+
activeView={activeView}
|
|
141
|
+
onViewChange={setActiveView}
|
|
142
|
+
previewUrl={state.previewUrl}
|
|
143
|
+
port={port}
|
|
144
|
+
onRefresh={restart}
|
|
145
|
+
/>
|
|
132
146
|
|
|
133
|
-
{/*
|
|
134
|
-
<
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
147
|
+
{/* Views — full width, toggled */}
|
|
148
|
+
<div className="flex-1 relative overflow-hidden min-h-0">
|
|
149
|
+
{/* Code view */}
|
|
150
|
+
<motion.div
|
|
151
|
+
className="absolute inset-0"
|
|
152
|
+
initial={false}
|
|
153
|
+
animate={{
|
|
154
|
+
opacity: activeView === "code" ? 1 : 0,
|
|
155
|
+
pointerEvents: activeView === "code" ? "auto" : "none",
|
|
156
|
+
}}
|
|
157
|
+
transition={{ duration: 0.15 }}
|
|
158
|
+
>
|
|
159
|
+
<CodeView
|
|
160
|
+
fileTree={fileTree}
|
|
161
|
+
files={state.files}
|
|
162
|
+
originalFiles={state.originalFiles}
|
|
163
|
+
fileChanges={state.fileChanges}
|
|
164
|
+
selectedFile={selectedFile}
|
|
165
|
+
openFiles={openFiles}
|
|
166
|
+
terminalOutput={state.terminalOutput}
|
|
167
|
+
onSelectFile={onFileSelect}
|
|
168
|
+
onCloseFile={handleCloseFile}
|
|
169
|
+
onFileChange={handleFileChange}
|
|
170
|
+
/>
|
|
171
|
+
</motion.div>
|
|
172
|
+
|
|
173
|
+
{/* Preview view */}
|
|
174
|
+
<motion.div
|
|
175
|
+
className="absolute inset-0"
|
|
176
|
+
initial={false}
|
|
177
|
+
animate={{
|
|
178
|
+
opacity: activeView === "preview" ? 1 : 0,
|
|
179
|
+
pointerEvents: activeView === "preview" ? "auto" : "none",
|
|
180
|
+
}}
|
|
181
|
+
transition={{ duration: 0.15 }}
|
|
182
|
+
>
|
|
183
|
+
<Preview
|
|
184
|
+
url={state.previewUrl}
|
|
185
|
+
onBrowserError={handleBrowserError}
|
|
186
|
+
/>
|
|
187
|
+
</motion.div>
|
|
188
|
+
</div>
|
|
145
189
|
</div>
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
190
|
+
);
|
|
191
|
+
},
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
CodeSandbox.displayName = "CodeSandbox";
|
|
149
195
|
|
|
150
196
|
// ---------------------------------------------------------------------------
|
|
151
197
|
// Toolbar
|